using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
/// <summary>
/// GAME 1303 Tier 1 — Side-scrolling player movement. (v1.1 — Tier 3 hooks added)
///
/// 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: coyote time, jump buffering, variable jump height, sprite flipping,
/// OnJumped / OnLanded events.
///
/// v1.1 adds hooks that Tier 3 modules (Dash, WallSlideJump, Ladder, Glide, Sprint,
/// DoubleJump, GrapplingHook, RailRide, MomentumLauncher) use:
/// MovementLocked – while true this script leaves velocity alone (a module owns it)
/// SpeedMultiplier – Sprint sets 1.5, etc.
/// CanCoyoteJump – true when a normal ground/coyote jump is available
/// ForceJump() – jump now, regardless of ground (double jump, wall jump)
/// ClearJumpBuffer(), JumpHeld, MoveVector, Facing
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController2D : MonoBehaviour
{
[Header("Movement")]
public float moveSpeed = 8f;
public float acceleration = 60f;
public float deceleration = 80f;
[Header("Jump")]
public float jumpForce = 14f;
public float coyoteTime = 0.1f;
public float jumpBufferTime = 0.1f;
[Range(0f, 1f)] public float jumpCutMultiplier = 0.5f;
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 UnityEvent OnJumped;
public UnityEvent OnLanded;
// --- Read-only state ---
public bool IsGrounded { get; private set; }
public bool FacingRight { get; private set; } = true;
public Vector2 Facing => FacingRight ? Vector2.right : Vector2.left;
public Vector2 Velocity => rb.linearVelocity;
public float MoveInput => moveInput.x;
public Vector2 MoveVector => moveInput;
public bool JumpHeld => jumpHeld;
public bool CanCoyoteJump => coyoteCounter > 0f;
public float BaseGravity => baseGravity;
/// <summary>Set false to freeze input (cutscenes, death, dialogue).</summary>
public bool InputEnabled { get; set; } = true;
/// <summary>A Tier 3 module sets this while it controls velocity (dash, wall slide, ladder, grapple, rail).</summary>
public bool MovementLocked { get; set; }
/// <summary>Sprint and slow effects multiply moveSpeed by this.</summary>
public float SpeedMultiplier { get; set; } = 1f;
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 ----------
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;
else if (!MovementLocked && rb.linearVelocity.y > 0f)
rb.linearVelocity = new Vector2(rb.linearVelocity.x, rb.linearVelocity.y * jumpCutMultiplier);
}
void Update()
{
IsGrounded = Physics2D.OverlapBox(groundCheck.position, groundCheckSize, 0f, groundLayer);
if (IsGrounded && !wasGrounded) OnLanded?.Invoke();
wasGrounded = IsGrounded;
coyoteCounter = IsGrounded ? coyoteTime : coyoteCounter - Time.deltaTime;
jumpBufferCounter -= Time.deltaTime;
if (!MovementLocked && jumpBufferCounter > 0f && coyoteCounter > 0f)
{
ForceJump(jumpForce);
}
if (!MovementLocked)
{
if (moveInput.x > 0.01f && !FacingRight) Flip();
else if (moveInput.x < -0.01f && FacingRight) Flip();
}
}
void FixedUpdate()
{
if (MovementLocked) return;
float target = moveInput.x * moveSpeed * SpeedMultiplier;
float rate = Mathf.Abs(target) > 0.01f ? acceleration : deceleration;
float newX = Mathf.MoveTowards(rb.linearVelocity.x, target, rate * Time.fixedDeltaTime);
rb.gravityScale = rb.linearVelocity.y < 0f ? baseGravity * fallGravityMultiplier : baseGravity;
float newY = Mathf.Max(rb.linearVelocity.y, -maxFallSpeed);
rb.linearVelocity = new Vector2(newX, newY);
}
// ---------- Public helpers for modules ----------
public void ForceJump(float force)
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x, force);
jumpBufferCounter = 0f;
coyoteCounter = 0f;
OnJumped?.Invoke();
}
public void ClearJumpBuffer() => jumpBufferCounter = 0f;
public void Flip()
{
FacingRight = !FacingRight;
Vector3 s = transform.localScale;
s.x *= -1f;
transform.localScale = s;
}
public void FaceDirection(float x)
{
if (x > 0.01f && !FacingRight) Flip();
else if (x < -0.01f && FacingRight) Flip();
}
public void ResetMotion()
{
rb.linearVelocity = Vector2.zero;
moveInput = Vector2.zero;
jumpBufferCounter = 0f;
MovementLocked = false;
SpeedMultiplier = 1f;
rb.gravityScale = baseGravity;
}
void OnDrawGizmosSelected()
{
if (groundCheck == null) return;
Gizmos.color = Color.green;
Gizmos.DrawWireCube(groundCheck.position, groundCheckSize);
}
}