using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
/// <summary>
/// GAME 1303 Tier 3 — Grind along a power line. Zap Attack. (Also: zip lines, rails.)
///
/// SETUP — the rail
/// 1. Empty GameObject "PowerLine" with an EdgeCollider2D (Is Trigger ON). Edit its points
/// to draw the wire. Put it on a layer "Rail". Add a LineRenderer or sprite to show it.
/// 2. Add the Rail script (Rail.cs) so RailRide can find it.
///
/// SETUP — the player
/// 3. Add RailRide to the player (PlayerController2D v1.1). Set Rail Layer.
/// 4. Player must be tagged Player and have a collider that can overlap the wire's trigger.
/// 5. Attach: automatic when you touch the line while falling onto it (Attach On Touch),
/// or hold Down to grab / Up to skip it. Jump to leave. The ride ends at either end.
/// 6. Ride Speed: fixed, in the direction you were moving (or facing if standing still).
/// Momentum Carry: keep the speed when you jump off.
/// 7. Animator: bool "IsRiding" if present.
/// </summary>
public class RailRide : MonoBehaviour
{
public LayerMask railLayer;
public float rideSpeed = 10f;
public bool attachOnTouch = true;
[Tooltip("Vertical offset so the sprite sits ON the wire rather than through it.")]
public float rideHeightOffset = 0.5f;
public float jumpOffForce = 14f;
[Range(0f, 1f)] public float momentumCarry = 1f;
public float reattachCooldown = 0.3f;
[Header("Feel")]
public string sfxAttach = "rail_on";
public UnityEvent OnAttached;
public UnityEvent OnDetached;
public bool IsRiding { get; private set; }
private PlayerController2D controller;
private Rigidbody2D rb;
private Animator anim;
private Health health;
private Vector2[] pts; // world-space rail points
private int segment; // current segment index (pts[segment] -> pts[segment+1])
private float t; // 0..1 along the segment
private int dir; // +1 toward higher index, -1 toward lower
private float cooldown;
private static readonly int RideHash = Animator.StringToHash("IsRiding");
private bool hasParam;
void Awake()
{
controller = GetComponent<PlayerController2D>();
rb = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
health = GetComponent<Health>();
if (anim != null) foreach (var p in anim.parameters) if (p.nameHash == RideHash) hasParam = true;
}
void Update() { cooldown -= Time.deltaTime; }
void OnTriggerEnter2D(Collider2D other) => TryAttach(other);
void OnTriggerStay2D(Collider2D other) { if (!IsRiding) TryAttach(other); }
void TryAttach(Collider2D other)
{
if (IsRiding || cooldown > 0f) return;
if ((railLayer.value & (1 << other.gameObject.layer)) == 0) return;
var r = other.GetComponent<Rail>();
if (r == null) return;
if (controller.MoveVector.y > 0.5f) return; // holding Up = skip the rail
if (!attachOnTouch && controller.MoveVector.y > -0.5f) return; // must hold Down to grab
if (controller.IsGrounded) return; // must be airborne
Attach(r);
}
void Attach(Rail r)
{
pts = r.WorldPoints;
if (pts.Length < 2) return;
// Nearest segment
float best = float.MaxValue;
for (int i = 0; i < pts.Length - 1; i++)
{
float tt; float d = DistToSegment(rb.position, pts[i], pts[i + 1], out tt);
if (d < best) { best = d; segment = i; t = tt; }
}
// Direction from current velocity or facing
float vx = rb.linearVelocity.x;
float facingX = controller.FacingRight ? 1f : -1f;
float sign = Mathf.Abs(vx) > 0.5f ? Mathf.Sign(vx) : facingX;
Vector2 seg = pts[segment + 1] - pts[segment];
dir = Mathf.Sign(seg.x) == sign ? 1 : -1;
IsRiding = true;
controller.MovementLocked = true;
rb.gravityScale = 0f;
rb.linearVelocity = Vector2.zero;
if (hasParam) anim.SetBool(RideHash, true);
if (!string.IsNullOrEmpty(sfxAttach)) AudioManager.Instance?.PlaySFX(sfxAttach);
OnAttached?.Invoke();
}
void FixedUpdate()
{
if (!IsRiding) return;
if (health != null && health.IsDead) { Detach(Vector2.zero); return; }
float remaining = rideSpeed * Time.fixedDeltaTime;
while (remaining > 0f)
{
Vector2 a = pts[segment], b = pts[segment + 1];
float segLen = Vector2.Distance(a, b);
float posAlong = t * segLen;
float target = posAlong + dir * remaining;
if (target >= 0f && target <= segLen) { t = target / segLen; remaining = 0f; }
else if (dir > 0)
{
remaining -= (segLen - posAlong);
if (segment + 1 >= pts.Length - 1) { EndOfRail(); return; }
segment++; t = 0f;
}
else
{
remaining -= posAlong;
if (segment <= 0) { EndOfRail(); return; }
segment--; t = 1f;
}
}
Vector2 p = Vector2.Lerp(pts[segment], pts[segment + 1], t) + Vector2.up * rideHeightOffset;
rb.MovePosition(p);
controller.FaceDirection(dir * Mathf.Sign((pts[segment + 1] - pts[segment]).x));
}
void OnJump(InputValue v)
{
if (!v.isPressed || !IsRiding) return;
Vector2 along = (pts[segment + 1] - pts[segment]).normalized * dir * rideSpeed * momentumCarry;
Detach(new Vector2(along.x, jumpOffForce));
controller.ClearJumpBuffer();
controller.OnJumped?.Invoke();
}
void EndOfRail()
{
Vector2 along = (pts[segment + 1] - pts[segment]).normalized * dir * rideSpeed * momentumCarry;
Detach(along);
}
void Detach(Vector2 exitVelocity)
{
IsRiding = false;
controller.MovementLocked = false;
rb.gravityScale = controller.BaseGravity;
rb.linearVelocity = exitVelocity;
cooldown = reattachCooldown;
if (hasParam) anim.SetBool(RideHash, false);
OnDetached?.Invoke();
}
static float DistToSegment(Vector2 p, Vector2 a, Vector2 b, out float t)
{
Vector2 ab = b - a;
float len2 = ab.sqrMagnitude;
t = len2 < 0.0001f ? 0f : Mathf.Clamp01(Vector2.Dot(p - a, ab) / len2);
return Vector2.Distance(p, a + ab * t);
}
}