using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
/// <summary>
/// GAME 1303 Tier 6 — Detective Liz's flashlight. Aims, drains a battery, recharges only
/// while you stand still (Jax's class idea), and hurts ghosts caught in the beam.
///
/// SETUP — the light
/// 1. Install URP and set up 2D lighting: Edit -> Project Settings -> Graphics with a
/// URP asset, and add a Global Light 2D to the scene set DARK (e.g. 0.15 intensity)
/// — that's what makes the building dark in the first place.
/// 2. On the player, create a child "Flashlight" with a Light 2D component set to
/// Spot (Freeform's "Point" type with narrow inner/outer angles). Drag it into Beam Light.
/// 3. Add this script to the PLAYER. Drag the Flashlight child into Beam Transform.
///
/// SETUP — the beam as a weapon
/// 4. On the same Flashlight child, add a Polygon/Cone-shaped Collider2D (Is Trigger)
/// roughly matching the light cone, plus a Tier 2 Hitbox (Target Team = Enemy,
/// Repeat Delay ~0.5). Drag it into Beam Hitbox. LightSensitiveEnemy reacts to it.
///
/// SETUP — the battery
/// 5. Add a Tier 3 FuelSystem to the player for the battery and drag it in — or leave it
/// empty and the flashlight never runs out (fine for early testing).
/// 6. Recharge Requires Standing Still is the interesting rule: the player has to stop
/// and be vulnerable to get light back. Recharge Rate is battery per second.
///
/// AIM MODES
/// Mouse – points at the cursor (best on PC).
/// Facing – points where the player faces (simplest, gamepad friendly).
/// Stick – right stick / Move vector.
/// </summary>
public class Flashlight2D : MonoBehaviour
{
public enum Aim { Mouse, Facing, Stick }
[Header("Beam")]
public Transform beamTransform;
[Tooltip("The Light 2D GameObject (any renderer/light — it's just enabled/disabled).")]
public GameObject beamLight;
public Hitbox beamHitbox;
public Aim aim = Aim.Mouse;
public float aimSmoothing = 12f;
public float rotationOffset = 0f;
[Header("Battery")]
public FuelSystem battery;
public float drainPerSecond = 4f;
public bool rechargeRequiresStandingStill = true;
public float rechargeRate = 8f;
[Tooltip("Must stand still this long before recharging starts.")]
public float rechargeDelay = 0.5f;
[Tooltip("Battery level below which the beam flickers as a warning (0-1).")]
[Range(0f, 1f)] public float flickerBelow = 0.2f;
[Header("Control")]
public bool startOn = true;
[Tooltip("Toggle with the Interact-adjacent 'Flashlight' action, or hold it.")]
public bool toggleMode = true;
[Header("Feedback")]
public string clickSfx = "flashlight_click";
public string emptySfx = "flashlight_empty";
[Header("Events")]
public UnityEvent OnTurnedOn;
public UnityEvent OnTurnedOff;
public UnityEvent OnBatteryEmpty;
public UnityEvent<bool> OnRechargingChanged;
public bool IsOn { get; private set; }
public bool IsRecharging { get; private set; }
private PlayerController2D platformer;
private TopDownMover topDown;
private Vector2 moveInput;
private Vector2 aimDirection = Vector2.right;
private float stillTimer;
private bool held;
void Awake()
{
platformer = GetComponent<PlayerController2D>();
topDown = GetComponent<TopDownMover>();
SetOn(startOn);
}
void OnMove(InputValue v) { moveInput = v.Get<Vector2>(); }
void OnFlashlight(InputValue v)
{
if (toggleMode) { if (v.isPressed) SetOn(!IsOn); }
else { held = v.isPressed; SetOn(held); }
}
void Update()
{
UpdateAim();
UpdateBattery();
}
void UpdateAim()
{
if (beamTransform == null) return;
Vector2 desired = aimDirection;
switch (aim)
{
case Aim.Mouse:
if (Mouse.current != null && Camera.main != null)
{
Vector3 m = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());
Vector2 d = (Vector2)m - (Vector2)transform.position;
if (d.sqrMagnitude > 0.01f) desired = d.normalized;
}
break;
case Aim.Stick:
if (moveInput.sqrMagnitude > 0.1f) desired = moveInput.normalized;
break;
default:
if (platformer != null) desired = platformer.Facing;
else if (topDown != null) desired = topDown.Facing2D;
break;
}
aimDirection = aimSmoothing > 0f
? Vector2.Lerp(aimDirection, desired, aimSmoothing * Time.deltaTime).normalized
: desired;
float angle = Mathf.Atan2(aimDirection.y, aimDirection.x) * Mathf.Rad2Deg + rotationOffset;
// The beam is a child of a sprite that may be flipped on X — undo that so the
// beam doesn't point backwards when the player faces left.
float flip = Mathf.Sign(transform.lossyScale.x);
beamTransform.rotation = Quaternion.Euler(0f, 0f, flip >= 0f ? angle : 180f - angle);
}
void UpdateBattery()
{
bool moving = moveInput.sqrMagnitude > 0.01f;
stillTimer = moving ? 0f : stillTimer + Time.deltaTime;
if (battery == null) return;
if (IsOn)
{
if (!battery.Consume(drainPerSecond * Time.deltaTime))
{
SetOn(false);
if (!string.IsNullOrEmpty(emptySfx)) AudioManager.Instance?.PlaySFX(emptySfx);
OnBatteryEmpty?.Invoke();
}
else if (flickerBelow > 0f && battery.Normalized < flickerBelow && beamLight != null)
{
beamLight.SetActive(Random.value > 0.25f);
}
SetRecharging(false);
return;
}
// Off: recharge, maybe only while standing still
bool canRecharge = !rechargeRequiresStandingStill || (stillTimer >= rechargeDelay);
SetRecharging(canRecharge && battery.Normalized < 1f);
if (IsRecharging) battery.Refill(rechargeRate * Time.deltaTime);
}
void SetRecharging(bool on)
{
if (on == IsRecharging) return;
IsRecharging = on;
OnRechargingChanged?.Invoke(on);
}
public void SetOn(bool on)
{
if (on && battery != null && battery.IsEmpty) on = false;
if (on == IsOn && Application.isPlaying) return;
IsOn = on;
if (beamLight != null) beamLight.SetActive(on);
if (beamHitbox != null) beamHitbox.gameObject.SetActive(on);
if (!string.IsNullOrEmpty(clickSfx)) AudioManager.Instance?.PlaySFX(clickSfx);
if (on) OnTurnedOn?.Invoke(); else OnTurnedOff?.Invoke();
}
/// <summary>Is a world position inside the beam? DarknessMeter and ghosts use this.</summary>
public bool IsLit(Vector3 worldPos, float coneAngle = 35f, float range = 8f)
{
if (!IsOn) return false;
Vector2 to = (Vector2)worldPos - (Vector2)transform.position;
if (to.magnitude > range) return false;
return Vector2.Angle(aimDirection, to) <= coneAngle;
}
}