using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
/// <summary>
/// GAME 1303 Tier 5 — Free 8-direction movement with no gravity. The top-down equivalent
/// of PlayerController2D. Last Delivery, Apep's Trial (top-down mode), Cozy Critters' pet.
///
/// SETUP
/// 1. Character: Sprite + Rigidbody2D (Gravity Scale 0, Freeze Rotation Z,
/// Collision Detection Continuous) + Collider2D + this script + PlayerInput
/// (Send Messages). Tag "Player" if it's the player.
/// 2. Walls need solid (non-trigger) colliders — the Rigidbody handles the sliding.
/// 3. Facing:
/// FlipSprite – flips on X like a side-scroller (simplest).
/// FourDirection – sets Animator ints/floats so you can have up/down/left/right
/// animation sets: MoveX, MoveY (floats, last non-zero direction)
/// plus Speed. This is the standard Zelda-style setup.
/// RotateSprite – points the sprite along movement (a delivery robot, a ship).
/// 4. AI Controlled: tick to ignore player input; drive it with SetMoveInput() from a
/// wander brain (Cozy Critters' PetBrain in Tier 7 does this).
///
/// This deliberately mirrors PlayerController2D's API — InputEnabled, MovementLocked,
/// SpeedMultiplier, ResetMotion — so Tier 1's RespawnManager and Tier 3 modules that
/// only need those still work.
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class TopDownMover : MonoBehaviour
{
public enum FacingMode { FlipSprite, FourDirection, RotateSprite, None }
[Header("Movement")]
public float moveSpeed = 5f;
public float acceleration = 50f;
public float deceleration = 60f;
[Tooltip("Normalize diagonals so you don't move 40% faster on the diagonal.")]
public bool normalizeDiagonal = true;
[Header("Facing")]
public FacingMode facing = FacingMode.FlipSprite;
public bool artFacesRight = true;
[Tooltip("RotateSprite mode: degrees to add if your art points up instead of right.")]
public float rotationOffset = 0f;
[Header("Control")]
public bool aiControlled = false;
[Header("Events")]
public UnityEvent OnStartedMoving;
public UnityEvent OnStoppedMoving;
// Mirrors PlayerController2D so shared systems work
public bool InputEnabled { get; set; } = true;
public bool MovementLocked { get; set; }
public float SpeedMultiplier { get; set; } = 1f;
public Vector2 Velocity => rb.linearVelocity;
public Vector2 MoveVector => moveInput;
public Vector2 Facing2D => lastDirection;
public bool IsMoving { get; private set; }
private Rigidbody2D rb;
private Animator anim;
private Vector2 moveInput;
private Vector2 lastDirection = Vector2.down;
private bool facingRight = true;
static readonly int SpeedHash = Animator.StringToHash("Speed");
static readonly int MoveXHash = Animator.StringToHash("MoveX");
static readonly int MoveYHash = Animator.StringToHash("MoveY");
private bool hasSpeed, hasMoveX, hasMoveY;
void Awake()
{
rb = GetComponent<Rigidbody2D>();
rb.gravityScale = 0f;
anim = GetComponent<Animator>();
if (anim != null)
foreach (var p in anim.parameters)
{
if (p.nameHash == SpeedHash) hasSpeed = true;
if (p.nameHash == MoveXHash) hasMoveX = true;
if (p.nameHash == MoveYHash) hasMoveY = true;
}
facingRight = artFacesRight ? transform.localScale.x > 0f : transform.localScale.x < 0f;
}
void OnMove(InputValue value)
{
if (aiControlled) return;
moveInput = InputEnabled ? value.Get<Vector2>() : Vector2.zero;
}
/// <summary>For AI: feed a direction (any length up to 1). Cozy Critters, enemies.</summary>
public void SetMoveInput(Vector2 input) => moveInput = Vector2.ClampMagnitude(input, 1f);
void FixedUpdate()
{
if (MovementLocked) return;
Vector2 input = moveInput;
if (normalizeDiagonal && input.sqrMagnitude > 1f) input = input.normalized;
Vector2 target = input * moveSpeed * SpeedMultiplier;
float rate = target.sqrMagnitude > 0.01f ? acceleration : deceleration;
rb.linearVelocity = Vector2.MoveTowards(rb.linearVelocity, target, rate * Time.fixedDeltaTime);
bool movingNow = input.sqrMagnitude > 0.01f;
if (movingNow)
{
lastDirection = input.normalized;
ApplyFacing();
}
if (movingNow != IsMoving)
{
IsMoving = movingNow;
if (movingNow) OnStartedMoving?.Invoke(); else OnStoppedMoving?.Invoke();
}
UpdateAnimator();
}
void ApplyFacing()
{
switch (facing)
{
case FacingMode.FlipSprite:
if (Mathf.Abs(lastDirection.x) < 0.01f) break;
bool wantRight = lastDirection.x > 0f;
if (wantRight != facingRight)
{
facingRight = wantRight;
Vector3 s = transform.localScale; s.x *= -1f; transform.localScale = s;
}
break;
case FacingMode.RotateSprite:
float angle = Mathf.Atan2(lastDirection.y, lastDirection.x) * Mathf.Rad2Deg + rotationOffset;
transform.rotation = Quaternion.Euler(0f, 0f, angle);
break;
}
// FourDirection is handled by the animator parameters below.
}
void UpdateAnimator()
{
if (anim == null) return;
if (hasSpeed) anim.SetFloat(SpeedHash, rb.linearVelocity.magnitude);
if (facing == FacingMode.FourDirection)
{
if (hasMoveX) anim.SetFloat(MoveXHash, lastDirection.x);
if (hasMoveY) anim.SetFloat(MoveYHash, lastDirection.y);
}
}
public void ResetMotion()
{
rb.linearVelocity = Vector2.zero;
moveInput = Vector2.zero;
MovementLocked = false;
SpeedMultiplier = 1f;
IsMoving = false;
}
}