MiniGames/Assets/CrateEscapeGameManager.cs
2025-08-14 20:29:09 +05:00

325 lines
12 KiB
C#
Raw Blame History

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<GameObject> levelObjects = new List<GameObject>();
[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<ZibuAnimDriver>();
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<6F>t double end if already completed
isGameOver = true;
Time.timeScale = 0f;
Debug.Log("[CrateEscape] GAME OVER");
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<CrateEscapePlayerControllerJoystick>();
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 ("<LayerName>.<StateName>"), 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<CratePlayerController>() : 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<77>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 ----------
/// <summary>Call this from the door trigger when the player enters.</summary>
public void OnLevelCompleteTriggered()
{
if (_levelComplete || isGameOver) return;
_levelComplete = true;
Time.timeScale = 0f;
if (LevelCompletePanel) LevelCompletePanel.SetActive(true);
Debug.Log("[CrateEscape] LEVEL COMPLETE");
}
/// <summary>Hook this to the Continue button on the Level Complete UI.</summary>
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);
}
}