using UnityEngine; using System.Collections; using UnityEngine.SceneManagement; using System.Collections.Generic; public class CrateEscapeGameManager : MonoBehaviour { public static CrateEscapeGameManager Instance { get; private set; } // ---------- Level Selection ---------- [Header("Levels")] [Tooltip("Drag all level GameObjects here (only one will be active).")] public List levelObjects = new List(); [Tooltip("PlayerPrefs key used to remember which level index is active.")] public string levelIndexPrefKey = "CrateEscape_LevelIndex"; [Tooltip("If true, will clamp/loop index at startup as well.")] public bool applyIndexOnAwake = true; // ---------- Animation (death) ---------- [Header("Animation (ZibuAnimDriver)")] public ZibuAnimDriver animDriver; // Assign your ZibuAnimDriver on the player public AnimationState deathState = AnimationState.Falling; // Play Falling on death [Range(0f, 1f)] public float deathCrossfade = 0.1f; [Tooltip("If > 0, this exact time (seconds) is used instead of auto-detecting the clip length.")] public float deathWaitOverride = 0f; [Header("Fallbacks")] [Tooltip("Used if clip length can't be determined and no override set.")] public float fallbackDeathDelay = 1.2f; // ---------- UI / State ---------- [Header("State/UI")] public bool isGameOver = false; public GameObject GameOverPanel; [Tooltip("Shown when the player reaches the door / finishes the level.")] public GameObject LevelCompletePanel; // Internal guards bool _isDying = false; bool _levelComplete = false; [Header("Death Anim (Direct State Name)")] public string deathStateName = "Falling"; // EXACT state name in Animator public int deathLayerIndex = 0; // Add somewhere near your other fields: [Header("Death Anim (robust)")] public string[] deathStateCandidates = new[] { "Falling", "Fall", "FallingLoop", "Squashed" }; public bool useUnscaledIfTimescaleZero = true; // play death even if timeScale == 0 void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; if (GameOverPanel) GameOverPanel.SetActive(false); if (LevelCompletePanel) LevelCompletePanel.SetActive(false); if (applyIndexOnAwake) ApplyLevelIndexFromPrefs(); if (!animDriver) { animDriver = FindObjectOfType(); if (!animDriver) Debug.LogWarning("[CrateEscape] No ZibuAnimDriver assigned to GameManager."); } } // ---------- LEVEL HANDLING ---------- public int GetCurrentLevelIndex() { return PlayerPrefs.GetInt(levelIndexPrefKey, 0); } public void ApplyLevelIndexFromPrefs() { if (levelObjects == null || levelObjects.Count == 0) return; int count = levelObjects.Count; int idx = GetCurrentLevelIndex(); idx = Mod(idx, count); for (int i = 0; i < count; i++) if (levelObjects[i]) levelObjects[i].SetActive(i == idx); } static int Mod(int a, int n) => (n == 0) ? 0 : ((a % n) + n) % n; // ---------- GAME OVER ---------- public void GameOver() { if (isGameOver || _levelComplete) return; // don’t double end if already completed isGameOver = true; Time.timeScale = 0f; if (GameOverPanel) GameOverPanel.SetActive(true); } public void Restarter() { Time.timeScale = 1f; Scene current = SceneManager.GetActiveScene(); SceneManager.LoadScene(current.buildIndex); } public void OnPlayerHitByLaser() { if (_isDying || isGameOver || _levelComplete) return; StartCoroutine(DeathSequenceThenGameOver()); // inside CrateEscapeGameManager when death begins: var joy = FindObjectOfType(); if (joy) joy.LockControls(true); } private void ForceEnterAnimatorState(Animator anim, int layer, string stateName, float xfade) { // Build both hashes: full path ("Base Layer.Falling") and short ("Falling") string layerName = anim.GetLayerName(layer); string fullPath = string.IsNullOrEmpty(layerName) ? stateName : $"{layerName}.{stateName}"; int fullHash = Animator.StringToHash(fullPath); int shortHash = Animator.StringToHash(stateName); bool hasFull = anim.HasState(layer, fullHash); bool hasShort = anim.HasState(layer, shortHash); // often false; Unity prefers full path // Try full path first if (hasFull) { anim.CrossFadeInFixedTime(fullHash, xfade, layer, 0f); // if still not in, hard play StartCoroutine(_EnsureState(anim, layer, fullHash, shortHash, stateName, xfade, triedFull: true)); return; } // Try short name next if (hasShort) { anim.CrossFadeInFixedTime(shortHash, xfade, layer, 0f); StartCoroutine(_EnsureState(anim, layer, shortHash, fullHash, stateName, xfade, triedFull: false)); return; } Debug.LogWarning($"[CrateEscape] Animator state not found: Layer={layerName} ({layer}), State='{stateName}'. " + "Check exact spelling/case or set 'deathStateName' to your real clip state name."); } private bool ForceEnterAny(Animator anim, int layer, string[] names, float xfade) { // Try full path first ("."), then short name. string layerName = anim.GetLayerName(layer); foreach (var name in names) { if (string.IsNullOrEmpty(name)) continue; string fullPath = string.IsNullOrEmpty(layerName) ? name : $"{layerName}.{name}"; int fullHash = Animator.StringToHash(fullPath); int shortHash = Animator.StringToHash(name); bool hasFull = anim.HasState(layer, fullHash); bool hasShort = anim.HasState(layer, shortHash); if (hasFull || hasShort) { int chosen = hasFull ? fullHash : shortHash; anim.CrossFadeInFixedTime(chosen, xfade, layer, 0f); // verify in a frame StartCoroutine(VerifyOrPlay(anim, layer, chosen, xfade)); return true; } } return false; } private IEnumerator VerifyOrPlay(Animator anim, int layer, int hash, float xfade) { yield return null; var st = anim.GetCurrentAnimatorStateInfo(layer); var nxt = anim.GetNextAnimatorStateInfo(layer); bool inTarget = st.shortNameHash == hash || (anim.IsInTransition(layer) && nxt.shortNameHash == hash); if (!inTarget) { anim.Play(hash, layer, 0f); yield return null; } } private IEnumerator _EnsureState(Animator anim, int layer, int primaryHash, int altHash, string stateName, float xfade, bool triedFull) { yield return null; // wait one frame var st = anim.GetCurrentAnimatorStateInfo(layer); var nxt = anim.GetNextAnimatorStateInfo(layer); bool inTarget = st.shortNameHash == primaryHash || (anim.IsInTransition(layer) && (nxt.shortNameHash == primaryHash)); if (!inTarget) { // try hard Play on the same hash anim.Play(primaryHash, layer, 0f); yield return null; st = anim.GetCurrentAnimatorStateInfo(layer); nxt = anim.GetNextAnimatorStateInfo(layer); inTarget = st.shortNameHash == primaryHash || (anim.IsInTransition(layer) && (nxt.shortNameHash == primaryHash)); if (!inTarget && altHash != 0) { // last resort: try the other hash type anim.CrossFadeInFixedTime(altHash, xfade, layer, 0f); yield return null; st = anim.GetCurrentAnimatorStateInfo(layer); nxt = anim.GetNextAnimatorStateInfo(layer); inTarget = st.shortNameHash == altHash || (anim.IsInTransition(layer) && (nxt.shortNameHash == altHash)); } } if (!inTarget) { string layerName = anim.GetLayerName(layer); Debug.LogWarning($"[CrateEscape] Could not enter '{stateName}' on layer '{layerName}' (idx {layer}). " + "Verify state name/layer. If it's inside a sub-state machine, the short name must still be EXACT."); } } private IEnumerator DeathSequenceThenGameOver() { _isDying = true; // 1) Stop anything that could fight the death state var ctrl = animDriver ? animDriver.GetComponent() : null; if (ctrl) ctrl.enabled = false; // 2) Try very hard to enter a valid death state Animator anim = animDriver ? animDriver.animator : null; if (anim) { anim.cullingMode = AnimatorCullingMode.AlwaysAnimate; anim.updateMode = AnimatorUpdateMode.Normal; // will switch to UnscaledTime below if needed animDriver.SetGrounded(false); animDriver.SetSpeed(0f); bool entered = ForceEnterAny(anim, deathLayerIndex, deathStateCandidates, deathCrossfade); yield return null; // let the state machine settle one frame if (!entered) Debug.LogWarning("[CrateEscape] Could not enter any death state. Check names/layer."); } // 3) Decide wait duration (prefer actual clip length if we’re in a real state) float wait = fallbackDeathDelay; if (deathWaitOverride > 0f) { wait = deathWaitOverride; } else if (anim) { var st = anim.GetCurrentAnimatorStateInfo(deathLayerIndex); var nxt = anim.GetNextAnimatorStateInfo(deathLayerIndex); float len = st.length; if (anim.IsInTransition(deathLayerIndex)) len = Mathf.Max(len, nxt.length); if (len > 0.05f) wait = len; } // 4) Wait (works even if someone set timeScale = 0) float clamped = Mathf.Max(0.05f, wait); if (useUnscaledIfTimescaleZero && Time.timeScale == 0f && anim) { var prev = anim.updateMode; anim.updateMode = AnimatorUpdateMode.UnscaledTime; yield return new WaitForSecondsRealtime(clamped); anim.updateMode = prev; } else { yield return new WaitForSeconds(clamped); } GameOver(); } // ---------- LEVEL COMPLETE FLOW ---------- /// Call this from the door trigger when the player enters. public void OnLevelCompleteTriggered() { if (_levelComplete || isGameOver) return; _levelComplete = true; Time.timeScale = 0f; if (LevelCompletePanel) LevelCompletePanel.SetActive(true); Debug.Log("[CrateEscape] LEVEL COMPLETE"); } /// Hook this to the Continue button on the Level Complete UI. public void ContinueToNextLevel() { if (levelObjects == null || levelObjects.Count == 0) { // No levels listed: just reload scene unchanged. Time.timeScale = 1f; Restarter(); return; } int count = levelObjects.Count; int idx = GetCurrentLevelIndex(); int next = Mod(idx + 1, count); PlayerPrefs.SetInt(levelIndexPrefKey, next); PlayerPrefs.Save(); Time.timeScale = 1f; SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); } }