using UnityEngine;
using UnityEngine.InputSystem;
/// <summary>
/// GAME 1303 Tier 1 — Side-scrolling player movement.
///
/// SETUP
/// 1. Add to your player GameObject along with: Rigidbody2D, a Collider2D, and a
/// PlayerInput component (Behavior = "Send Messages", Default Map = "Player").
/// 2. Create an empty child called "GroundCheck" at the player's feet and drag it
/// into the Ground Check slot. Set Ground Layer to whatever layer your floors use.
/// 3. In Rigidbody2D: Freeze Rotation Z, Collision Detection = Continuous,
/// Gravity Scale ≈ 3 for a snappy feel.
/// 4. The default Input Actions asset already has "Move" (Vector2) and "Jump" (Button)
/// in the "Player" map — this script listens for those names.
///
/// FEATURES students get for free: coyote time, jump buffering, variable jump height,
/// sprite flipping, and an OnJumped event other scripts (audio, animation) can hook.
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController2D : MonoBehaviour
{
[Header("Movement")]
[Tooltip("Horizontal speed in units per second.")]
public float moveSpeed = 8f;
[Tooltip("How quickly the player reaches full speed. Higher = tighter.")]
public float acceleration = 60f;
[Tooltip("How quickly the player stops. Higher = less slide.")]
public float deceleration = 80f;
[Header("Jump")]
public float jumpForce = 14f;
[Tooltip("Seconds after leaving a ledge during which you can still jump.")]
public float coyoteTime = 0.1f;
[Tooltip("Seconds before landing during which a jump press is remembered.")]
public float jumpBufferTime = 0.1f;
[Tooltip("Releasing Jump early multiplies upward velocity by this (0-1). 1 = fixed-height jumps.")]
[Range(0f, 1f)] public float jumpCutMultiplier = 0.5f;
[Tooltip("Extra gravity while falling makes jumps feel less floaty.")]
public float fallGravityMultiplier = 1.8f;
public float maxFallSpeed = 25f;
[Header("Ground Check")]
public Transform groundCheck;
public Vector2 groundCheckSize = new Vector2(0.8f, 0.1f);
public LayerMask groundLayer;
[Header("Events")]
public UnityEngine.Events.UnityEvent OnJumped;
public UnityEngine.Events.UnityEvent OnLanded;
// --- Read-only state other scripts can look at (Animator bridge, Dash, etc.) ---
public bool IsGrounded { get; private set; }
public bool FacingRight { get; private set; } = true;
public Vector2 Velocity => rb.linearVelocity;
public float MoveInput => moveInput.x;
/// <summary>Set false to freeze input (cutscenes, death, dialogue). Physics still runs.</summary>
public bool InputEnabled { get; set; } = true;
private Rigidbody2D rb;
private Vector2 moveInput;
private float coyoteCounter;
private float jumpBufferCounter;
private bool jumpHeld;
private bool wasGrounded;
private float baseGravity;
void Awake()
{
rb = GetComponent<Rigidbody2D>();
baseGravity = rb.gravityScale;
}
// ---------- Input System messages (PlayerInput -> Send Messages) ----------
void OnMove(InputValue value)
{
moveInput = InputEnabled ? value.Get<Vector2>() : Vector2.zero;
}
void OnJump(InputValue value)
{
if (!InputEnabled) return;
jumpHeld = value.isPressed;
if (jumpHeld) jumpBufferCounter = jumpBufferTime; // remember the press
else if (rb.linearVelocity.y > 0f) // released early -> cut the jump
rb.linearVelocity = new Vector2(rb.linearVelocity.x, rb.linearVelocity.y * jumpCutMultiplier);
}
void Update()
{
// Ground check
IsGrounded = Physics2D.OverlapBox(groundCheck.position, groundCheckSize, 0f, groundLayer);
if (IsGrounded && !wasGrounded) OnLanded?.Invoke();
wasGrounded = IsGrounded;
// Timers
coyoteCounter = IsGrounded ? coyoteTime : coyoteCounter - Time.deltaTime;
jumpBufferCounter -= Time.deltaTime;
// Jump when a buffered press meets coyote-eligible ground
if (jumpBufferCounter > 0f && coyoteCounter > 0f)
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
jumpBufferCounter = 0f;
coyoteCounter = 0f;
OnJumped?.Invoke();
}
// Flip sprite
if (moveInput.x > 0.01f && !FacingRight) Flip();
else if (moveInput.x < -0.01f && FacingRight) Flip();
}
void FixedUpdate()
{
// Horizontal: accelerate toward target speed
float target = moveInput.x * moveSpeed;
float rate = Mathf.Abs(target) > 0.01f ? acceleration : deceleration;
float newX = Mathf.MoveTowards(rb.linearVelocity.x, target, rate * Time.fixedDeltaTime);
// Vertical: heavier gravity when falling, clamp fall speed
rb.gravityScale = rb.linearVelocity.y < 0f ? baseGravity * fallGravityMultiplier : baseGravity;
float newY = Mathf.Max(rb.linearVelocity.y, -maxFallSpeed);
rb.linearVelocity = new Vector2(newX, newY);
}
void Flip()
{
FacingRight = !FacingRight;
Vector3 s = transform.localScale;
s.x *= -1f;
transform.localScale = s;
}
/// <summary>Instantly stop all motion (used by RespawnManager).</summary>
public void ResetMotion()
{
rb.linearVelocity = Vector2.zero;
moveInput = Vector2.zero;
jumpBufferCounter = 0f;
}
void OnDrawGizmosSelected()
{
if (groundCheck == null) return;
Gizmos.color = Color.green;
Gizmos.DrawWireCube(groundCheck.position, groundCheckSize);
}
}