325 lines
12 KiB
C#
325 lines
12 KiB
C#
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);
|
||
}
|
||
}
|