using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
/// <summary>
/// GAME 1303 Tier 5 — Movement in discrete steps, snapped to a grid. Weenie Maze (the
/// snake head), Last Delivery if Chris goes lane-based like Crossy Road.
///
/// Two very different feels, one script:
/// Mode = Stepped – you press a direction, the character slides one cell and stops.
/// Crossy Road, Pokémon, sokoban puzzles.
/// Mode = Continuous – the character keeps moving in the last direction forever; input
/// only CHANGES direction. This is Snake. Use with SnakeBody.
///
/// SETUP
/// 1. Character: Sprite + Rigidbody2D (Gravity 0, Kinematic is fine) + Collider2D +
/// this script + PlayerInput (Send Messages).
/// 2. Cell Size must match your Tilemap's cell size (usually 1).
/// 3. Blocking Layers: walls. In Stepped mode a blocked step is refused (you don't move);
/// in Continuous mode hitting a wall fires OnBlocked — wire it to game over, or to
/// stop the snake.
/// 4. Snap On Start puts the character exactly on a cell centre so everything lines up.
/// 5. Step Time: seconds per cell. In Continuous mode this is the snake's speed —
/// 0.15 is brisk, 0.25 is gentle. Speed Up Per Step lets it accelerate as you grow.
///
/// The character always finishes a step before starting the next, so movement stays on
/// the grid even if the player mashes keys. Queued Input remembers one press during a
/// step, which makes tight turns feel fair.
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class GridMover : MonoBehaviour
{
public enum Mode { Stepped, Continuous }
public Mode mode = Mode.Stepped;
public float cellSize = 1f;
public float stepTime = 0.15f;
[Tooltip("Continuous mode: each step gets this much faster (seconds). 0 = constant speed.")]
public float speedUpPerStep = 0f;
public float minStepTime = 0.06f;
public LayerMask blockingLayers;
public bool snapOnStart = true;
[Tooltip("Continuous mode: can't reverse straight into yourself.")]
public bool preventReversal = true;
[Header("Facing")]
public bool rotateToDirection = false;
public float rotationOffset = 0f;
public bool flipSprite = true;
[Header("Events")]
public UnityEvent<Vector2Int> OnStepStarted; // direction of the step
public UnityEvent<Vector3> OnStepCompleted; // the cell we just arrived at
public UnityEvent OnBlocked; // ran into a wall
public UnityEvent<Vector2Int> OnDirectionChanged;
public Vector2Int Direction { get; private set; } = Vector2Int.right;
public bool IsMoving { get; private set; }
public bool InputEnabled { get; set; } = true;
/// <summary>Position at the START of the current step — SnakeBody follows this.</summary>
public Vector3 PreviousCell { get; private set; }
public int StepCount { get; private set; }
private Rigidbody2D rb;
private Vector3 stepFrom, stepTo;
private float stepProgress;
private Vector2Int queuedDirection;
private bool hasQueued;
private float currentStepTime;
void Awake()
{
rb = GetComponent<Rigidbody2D>();
rb.gravityScale = 0f;
currentStepTime = stepTime;
}
void Start()
{
if (snapOnStart) transform.position = Snap(transform.position);
PreviousCell = transform.position;
stepFrom = stepTo = transform.position;
if (mode == Mode.Continuous) TryStartStep(Direction);
}
void OnMove(InputValue value)
{
if (!InputEnabled) return;
Vector2 v = value.Get<Vector2>();
if (v.sqrMagnitude < 0.25f) return;
// Snap to the dominant axis — no diagonals on a grid
Vector2Int dir = Mathf.Abs(v.x) > Mathf.Abs(v.y)
? new Vector2Int((int)Mathf.Sign(v.x), 0)
: new Vector2Int(0, (int)Mathf.Sign(v.y));
if (mode == Mode.Continuous && preventReversal && dir == -Direction) return;
if (IsMoving) { queuedDirection = dir; hasQueued = true; }
else SetDirection(dir);
}
void SetDirection(Vector2Int dir)
{
if (dir != Direction)
{
Direction = dir;
ApplyFacing();
OnDirectionChanged?.Invoke(dir);
}
if (mode == Mode.Stepped) TryStartStep(dir);
}
void Update()
{
if (IsMoving)
{
stepProgress += Time.deltaTime / Mathf.Max(0.01f, currentStepTime);
if (stepProgress >= 1f)
{
transform.position = stepTo;
IsMoving = false;
StepCount++;
OnStepCompleted?.Invoke(stepTo);
// Apply a queued turn, then continue if we're in Continuous mode
if (hasQueued)
{
hasQueued = false;
if (!(mode == Mode.Continuous && preventReversal && queuedDirection == -Direction))
{
Direction = queuedDirection;
ApplyFacing();
OnDirectionChanged?.Invoke(Direction);
}
}
if (mode == Mode.Continuous)
{
if (speedUpPerStep > 0f)
currentStepTime = Mathf.Max(minStepTime, currentStepTime - speedUpPerStep);
TryStartStep(Direction);
}
}
else
{
transform.position = Vector3.Lerp(stepFrom, stepTo, stepProgress);
}
}
}
bool TryStartStep(Vector2Int dir)
{
if (IsMoving) return false;
Vector3 from = Snap(transform.position);
Vector3 to = from + new Vector3(dir.x, dir.y, 0f) * cellSize;
// Wall check: a box just smaller than a cell at the destination
Collider2D hit = Physics2D.OverlapBox(to, Vector2.one * cellSize * 0.8f, 0f, blockingLayers);
if (hit != null)
{
OnBlocked?.Invoke();
return false;
}
PreviousCell = from;
stepFrom = from;
stepTo = to;
stepProgress = 0f;
IsMoving = true;
OnStepStarted?.Invoke(dir);
return true;
}
/// <summary>Force a direction from code (AI, a redirect tile).</summary>
public void ForceDirection(Vector2Int dir) => SetDirection(dir);
/// <summary>Stop dead (game over, cutscene).</summary>
public void Halt()
{
IsMoving = false;
hasQueued = false;
transform.position = Snap(transform.position);
}
public void ResetSpeed() => currentStepTime = stepTime;
Vector3 Snap(Vector3 p) => new Vector3(
Mathf.Round(p.x / cellSize) * cellSize,
Mathf.Round(p.y / cellSize) * cellSize,
p.z);
void ApplyFacing()
{
if (rotateToDirection)
{
float angle = Mathf.Atan2(Direction.y, Direction.x) * Mathf.Rad2Deg + rotationOffset;
transform.rotation = Quaternion.Euler(0f, 0f, angle);
}
else if (flipSprite && Direction.x != 0)
{
Vector3 s = transform.localScale;
s.x = Mathf.Abs(s.x) * (Direction.x > 0 ? 1f : -1f);
transform.localScale = s;
}
}
}