using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// GAME 1303 Tier 6 — A crate you can shove around to reach a ledge or hold a pressure
/// plate. Anabasis, Apep's Trial.
///
/// SETUP
/// 1. Crate: Sprite + Rigidbody2D + BoxCollider2D (solid) + this script.
/// 2. Physics Material: make a 2D Physics Material with Friction 0.4 and drop it on the
/// collider, or the crate slides forever.
/// 3. Mode:
/// Physics – the player just walks into it and it slides (Rigidbody Dynamic, high
/// Linear Damping, high Mass). Simple and forgiving.
/// Grid – snaps one cell per push (Rigidbody Kinematic). Sokoban-precise; set
/// Cell Size and Blocking Layers.
/// 4. Only Push Horizontally stops the player shoving crates off ledges by jumping on them.
/// 5. Reset On Respawn: the crate returns to its start position when the player dies,
/// so a puzzle can't be made unsolvable. Wire RespawnManager.OnRespawned ->
/// PushBlock.ResetPosition. (Strongly recommended — students forget this and then
/// wonder why testers get stuck.)
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class PushBlock : MonoBehaviour
{
public enum Mode { Physics, Grid }
public Mode mode = Mode.Physics;
public bool onlyPushHorizontally = true;
public string pushSfx = "push";
[Header("Physics mode")]
[Tooltip("How hard the player has to push. Higher mass = heavier feel.")]
public float mass = 4f;
public float linearDamping = 5f;
[Header("Grid mode")]
public float cellSize = 1f;
public float stepTime = 0.15f;
public LayerMask blockingLayers;
[Header("Events")]
public UnityEvent OnPushed;
public UnityEvent OnLanded;
private Rigidbody2D rb;
private Vector3 startPosition;
private bool stepping;
private Vector3 stepFrom, stepTo;
private float stepProgress;
void Awake()
{
rb = GetComponent<Rigidbody2D>();
startPosition = transform.position;
if (mode == Mode.Physics)
{
rb.bodyType = RigidbodyType2D.Dynamic;
rb.mass = mass;
rb.linearDamping = linearDamping;
rb.freezeRotation = true;
}
else
{
rb.bodyType = RigidbodyType2D.Kinematic;
transform.position = Snap(transform.position);
}
}
void Update()
{
if (mode != Mode.Grid || !stepping) return;
stepProgress += Time.deltaTime / Mathf.Max(0.01f, stepTime);
if (stepProgress >= 1f)
{
transform.position = stepTo;
stepping = false;
OnLanded?.Invoke();
}
else transform.position = Vector3.Lerp(stepFrom, stepTo, stepProgress);
}
void OnCollisionStay2D(Collision2D col)
{
if (mode != Mode.Grid || stepping) return;
if (!col.collider.CompareTag("Player")) return;
// Which way is the player pushing? Use the contact normal.
Vector2 n = col.GetContact(0).normal;
Vector2Int dir = Mathf.Abs(n.x) > Mathf.Abs(n.y)
? new Vector2Int(n.x > 0 ? -1 : 1, 0)
: new Vector2Int(0, n.y > 0 ? -1 : 1);
if (onlyPushHorizontally && dir.y != 0) return;
TryStep(dir);
}
public bool TryStep(Vector2Int dir)
{
if (stepping) return false;
Vector3 from = Snap(transform.position);
Vector3 to = from + new Vector3(dir.x, dir.y, 0f) * cellSize;
if (Physics2D.OverlapBox(to, Vector2.one * cellSize * 0.8f, 0f, blockingLayers) != null) return false;
stepFrom = from;
stepTo = to;
stepProgress = 0f;
stepping = true;
if (!string.IsNullOrEmpty(pushSfx)) AudioManager.Instance?.PlaySFX(pushSfx);
OnPushed?.Invoke();
return true;
}
/// <summary>Wire RespawnManager.OnRespawned here so puzzles can't be softlocked.</summary>
public void ResetPosition()
{
transform.position = startPosition;
if (rb != null) { rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; }
stepping = false;
}
Vector3 Snap(Vector3 p) => new Vector3(
Mathf.Round(p.x / cellSize) * cellSize,
Mathf.Round(p.y / cellSize) * cellSize,
p.z);
void OnDrawGizmosSelected()
{
Gizmos.color = Color.green;
Gizmos.DrawWireCube(Application.isPlaying ? startPosition : transform.position, Vector3.one * 0.9f);
}
}