using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
/// <summary>
/// GAME 1303 Tier 6 — Stairs, elevators, and doorways that move the player between floors
/// or rooms WITHIN one scene. Detective Liz's five-story building.
///
/// WHY NOT JUST LOAD A SCENE: five scenes for five floors means five copies of the player,
/// the HUD, the flashlight state, and the ghosts. Keeping one scene and teleporting between
/// floor areas is far less to manage — and the battery, dread meter, and clue flags all
/// survive automatically.
///
/// SETUP
/// 1. Lay out each floor in the same scene, stacked vertically or side by side with plenty
/// of space between them.
/// 2. Stairs at floor 1: empty GameObject + Collider2D (Is Trigger) + this script.
/// Drag the matching stairs object on floor 2 into Destination. On that one, drag this
/// one back — two-way stairs are just two of these pointing at each other.
/// 3. Mode:
/// OnEnter – walk into it and you go (doorways).
/// OnInteract – needs the Interact key; add an Interactable to the same object and
/// wire OnInteract -> FloorTransition.Use (stairs, elevator buttons).
/// 4. Fade: uses SceneLoader's fade image if one exists, otherwise it's instant.
/// 5. Camera: with a Cinemachine Confiner per floor, drag the destination floor's
/// bounding shape into New Confiner Shape and the camera re-frames on arrival.
/// 6. Set Flag On Use records that the player reached this floor — useful for both the
/// ending logic and a "you've explored 4 of 5 floors" readout.
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class FloorTransition : MonoBehaviour
{
public enum Mode { OnEnter, OnInteract }
public Mode mode = Mode.OnInteract;
[Tooltip("Where the player comes out. Usually another FloorTransition's transform.")]
public Transform destination;
public float fadeTime = 0.3f;
public float lockTime = 0.4f;
[Header("Camera (optional)")]
[Tooltip("The destination floor's Cinemachine Confiner 2D bounding shape.")]
public Collider2D newConfinerShape;
[Tooltip("Drag the CinemachineConfiner2D component's GameObject here.")]
public GameObject confinerObject;
[Header("Story")]
public string setFlagOnUse = "";
[Tooltip("Flags required to use it (! to negate). e.g. 'has_elevator_key'")]
public string requireFlags = "";
[Header("Feedback")]
public string useSfx = "stairs";
public string blockedSfx = "door_locked";
[Header("Events")]
public UnityEvent OnUsed;
public UnityEvent OnBlocked;
private bool busy;
private static float globalCooldown;
void Reset() { GetComponent<Collider2D>().isTrigger = true; }
void Update() { if (globalCooldown > 0f) globalCooldown -= Time.deltaTime; }
void OnTriggerEnter2D(Collider2D other)
{
if (mode != Mode.OnEnter || !other.CompareTag("Player")) return;
Use();
}
/// <summary>Wire an Interactable's OnInteract here for stairs you press E on.</summary>
public void Use()
{
if (busy || globalCooldown > 0f || destination == null) return;
if (!string.IsNullOrWhiteSpace(requireFlags) &&
ChoiceFlagManager.Instance != null && !ChoiceFlagManager.Instance.HasAll(requireFlags))
{
if (!string.IsNullOrEmpty(blockedSfx)) AudioManager.Instance?.PlaySFX(blockedSfx);
OnBlocked?.Invoke();
return;
}
var player = GameObject.FindGameObjectWithTag("Player");
if (player == null) return;
StartCoroutine(Travel(player));
}
IEnumerator Travel(GameObject player)
{
busy = true;
globalCooldown = lockTime;
var pc = player.GetComponent<PlayerController2D>();
var td = player.GetComponent<TopDownMover>();
if (pc != null) pc.InputEnabled = false;
if (td != null) td.InputEnabled = false;
if (!string.IsNullOrEmpty(useSfx)) AudioManager.Instance?.PlaySFX(useSfx);
// Fade out using SceneLoader's image if there is one
var fade = SceneLoader.Instance != null ? SceneLoader.Instance.fadeImage : null;
if (fade != null && fadeTime > 0f)
{
fade.gameObject.SetActive(true);
yield return FadeTo(fade, 1f);
}
player.transform.position = destination.position;
if (pc != null) pc.ResetMotion();
if (td != null) td.ResetMotion();
SwapConfiner();
if (!string.IsNullOrEmpty(setFlagOnUse)) ChoiceFlagManager.Instance?.SetFlag(setFlagOnUse);
yield return null; // let the camera settle before fading back in
if (fade != null && fadeTime > 0f)
{
yield return FadeTo(fade, 0f);
fade.gameObject.SetActive(false);
}
if (pc != null) pc.InputEnabled = true;
if (td != null) td.InputEnabled = true;
OnUsed?.Invoke();
busy = false;
}
IEnumerator FadeTo(UnityEngine.UI.Image img, float target)
{
float from = img.color.a;
float t = 0f;
while (t < fadeTime)
{
t += Time.unscaledDeltaTime;
var c = img.color;
c.a = Mathf.Lerp(from, target, t / fadeTime);
img.color = c;
yield return null;
}
var final = img.color;
final.a = target;
img.color = final;
}
void SwapConfiner()
{
if (confinerObject == null || newConfinerShape == null) return;
// Set the bounding shape by reflection so this script doesn't hard-require
// Cinemachine at compile time (a student without it installed still builds).
var confiner = confinerObject.GetComponent("CinemachineConfiner2D");
if (confiner == null) return;
var prop = confiner.GetType().GetProperty("BoundingShape2D")
?? confiner.GetType().GetProperty("m_BoundingShape2D");
if (prop != null) prop.SetValue(confiner, newConfinerShape);
var field = confiner.GetType().GetField("BoundingShape2D")
?? confiner.GetType().GetField("m_BoundingShape2D");
if (field != null) field.SetValue(confiner, newConfinerShape);
var invalidate = confiner.GetType().GetMethod("InvalidateBoundingShapeCache")
?? confiner.GetType().GetMethod("InvalidateCache");
invalidate?.Invoke(confiner, null);
}
void OnDrawGizmos()
{
if (destination == null) return;
Gizmos.color = new Color(0.6f, 0.4f, 1f, 0.9f);
Gizmos.DrawLine(transform.position, destination.position);
Gizmos.DrawWireSphere(destination.position, 0.3f);
}
}