using UnityEngine; using System.Collections; public class ChaseRunEnemy : MonoBehaviour { [Header("Refs")] public Animator m_animator; public Rigidbody rb; [Header("Movement")] public float moveSpeed = 4f; [Tooltip("Enter stomp when within this distance to player.")] public float stopDistance = 1.5f; [Tooltip("Resume run if player is at least this far (hysteresis). Must be > stopDistance.")] public float resumeDistance = 2.2f; [Header("Stomp Resume Control")] [Tooltip("If player escapes by at least this many meters beyond closest point during stomp, resume.")] public float escapeDelta = 0.75f; [Tooltip("Minimum time to wait in stomp before allowing resume (prevents instant pop-back).")] public float stompTimeout = 0.6f; [Tooltip("Absolute max time to hold stomp before forcing resume if no squish happens.")] public float maxStompHold = 2.0f; [Header("Animator Parameters")] [Tooltip("Optional idle/stop bool (can block Stomp). Leave empty to ignore.")] public string isStopBoolName = "isStop"; [Tooltip("Bool that transitions Run -> Stomp/Squish in your Animator.")] public string stompBoolName = "Stomp"; [Header("Animator State Fallback (optional)")] [Tooltip("Name of your stomp/squish state for forced crossfade if the bool doesn’t enter.")] public string stompStateName = ""; // e.g. "Stomp" or "Squish" [Tooltip("Animator base layer index.")] public int baseLayer = 0; [Header("Speed Source")] public bool usePlayerSpeed = false; // keep false so enemy doesn't inherit wolf boost [Header("VFX (optional)")] [SerializeField] private ParticleSystem LeftFootDustPoof, RightFootDustPoof; [Header("Squish Fallback (optional)")] [Tooltip("If true, also call KillBySquish after a short delay in case the foot trigger misses.")] public bool ensureSquishFallback = false; [Tooltip("Delay that matches the stomp impact frame in your animation.")] public float stompImpactDelay = 0.25f; [Header("Debug")] public bool debugLogs = false; private Transform player; private ChasePlayerController playerController; private float stopDistanceSqr, resumeDistanceSqr, escapeDeltaSqr; private bool stompTriggered = false; private float stompStartTime = -999f; private float minDistanceSqrDuringStomp = float.PositiveInfinity; private int stompStateHash = 0; private void Awake() { if (!rb) rb = GetComponent(); var playerObj = GameObject.FindGameObjectWithTag("Player"); if (playerObj) { player = playerObj.transform; playerController = player.GetComponentInParent(); } else { Debug.LogError("[ChaseRunEnemy] Player not found in scene."); } stopDistanceSqr = stopDistance * stopDistance; resumeDistanceSqr = resumeDistance * resumeDistance; escapeDeltaSqr = escapeDelta * escapeDelta; if (!string.IsNullOrEmpty(stompStateName)) stompStateHash = Animator.StringToHash(stompStateName); if (resumeDistance <= stopDistance) Debug.LogWarning("[ChaseRunEnemy] resumeDistance should be > stopDistance to avoid flip-flop."); } private void OnEnable() { if (usePlayerSpeed) ChasePlayerController.OnMoveSpeedChanged += UpdateMoveSpeed; } private void OnDisable() { if (usePlayerSpeed) ChasePlayerController.OnMoveSpeedChanged -= UpdateMoveSpeed; // correct unsubscribe } private void UpdateMoveSpeed(float speed) => moveSpeed = speed; private void FixedUpdate() { if (!player) return; Vector3 toPlayer = player.position - transform.position; float distanceSqr = toPlayer.sqrMagnitude; if (stompTriggered) { // Track closest approach while in stomp if (distanceSqr < minDistanceSqrDuringStomp) minDistanceSqrDuringStomp = distanceSqr; // If player actually died, do nothing—let animation/foot trigger finish if (playerController && playerController.waitingForGameOver) return; // Resume conditions: bool waitedEnough = (Time.time - stompStartTime) >= stompTimeout; bool farEnoughByResume = distanceSqr > resumeDistanceSqr; bool farEnoughByEscape = distanceSqr > (minDistanceSqrDuringStomp + escapeDeltaSqr); bool forceTimeout = (Time.time - stompStartTime) >= maxStompHold; if ((waitedEnough && (farEnoughByResume || farEnoughByEscape)) || forceTimeout) { if (debugLogs) Debug.Log("[ChaseRunEnemy] Resuming run (no squish)."); ClearStompAndResume(); return; } // Stay stopped (keep zero velocity) if (rb) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } return; } // Not stomping yet: check entry condition if (distanceSqr <= stopDistanceSqr) { TriggerStomp(distanceSqr); return; } // Otherwise, keep running forward MoveForward(); } private void MoveForward() { rb.MovePosition(rb.position + Vector3.back * moveSpeed * Time.fixedDeltaTime); } private void TriggerStomp(float currentDistanceSqr) { if (stompTriggered) return; stompTriggered = true; stompStartTime = Time.time; minDistanceSqrDuringStomp = currentDistanceSqr; // Stop instantly via physics — DO NOT set isStop yet (it may block your stomp transition) if (rb) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } if (m_animator) { // 1) Request stomp first if (!string.IsNullOrEmpty(stompBoolName)) m_animator.SetBool(stompBoolName, true); // 2) Optionally mark stop AFTER stomp gets a frame to take effect (handled in coroutine) if (!string.IsNullOrEmpty(isStopBoolName)) m_animator.SetBool(isStopBoolName, false); // make sure it isn't blocking // Verify we actually enter stomp; if not, fix animator conditions or force crossfade. StartCoroutine(EnsureEnteredStomp()); } if (debugLogs) Debug.Log("[ChaseRunEnemy] Enter Stomp request sent."); // Optional safety: squish fallback if foot trigger misses if (ensureSquishFallback) Invoke(nameof(FallbackSquishPlayer), stompImpactDelay); } private IEnumerator EnsureEnteredStomp() { // Wait one frame so Animator can process the bool yield return null; if (IsInOrGoingTo(stompStateHash)) { // Now that stomp transition started, you MAY set isStop=true if you still need it for other graphs if (!string.IsNullOrEmpty(isStopBoolName)) m_animator.SetBool(isStopBoolName, true); if (debugLogs) Debug.Log("[ChaseRunEnemy] Stomp detected on Animator."); yield break; } // If stomp not entered, try unblocking by ensuring isStop is false if (!string.IsNullOrEmpty(isStopBoolName)) m_animator.SetBool(isStopBoolName, false); // Give it another frame yield return null; if (IsInOrGoingTo(stompStateHash)) { if (!string.IsNullOrEmpty(isStopBoolName)) m_animator.SetBool(isStopBoolName, true); if (debugLogs) Debug.Log("[ChaseRunEnemy] Stomp detected after clearing isStop."); yield break; } // Still not in stomp — force crossfade if we have a state name if (stompStateHash != 0) { m_animator.CrossFadeInFixedTime(stompStateHash, 0.08f, baseLayer, 0f); if (debugLogs) Debug.Log("[ChaseRunEnemy] Forced crossfade to stomp state."); // Optional: set isStop after forcing if (!string.IsNullOrEmpty(isStopBoolName)) m_animator.SetBool(isStopBoolName, true); } else { if (debugLogs) Debug.LogWarning("[ChaseRunEnemy] Stomp not entered and no stompStateName set for fallback."); } } private void ClearStompAndResume() { stompTriggered = false; if (m_animator) { if (!string.IsNullOrEmpty(isStopBoolName)) m_animator.SetBool(isStopBoolName, false); if (!string.IsNullOrEmpty(stompBoolName)) m_animator.SetBool(stompBoolName, false); } } private void FallbackSquishPlayer() { if (!playerController /*|| playerController.waitingForGameOver*/) return; if (debugLogs) Debug.Log("[ChaseRunEnemy] Fallback KillBySquish."); playerController.KillBySquish(null, 0.25f, 0.35f); } // ===== Helpers ===== private bool IsInOrGoingTo(int shortHash) { if (m_animator == null || shortHash == 0) return false; var st = m_animator.GetCurrentAnimatorStateInfo(baseLayer); if (st.shortNameHash == shortHash) return true; if (m_animator.IsInTransition(baseLayer)) { var nxt = m_animator.GetNextAnimatorStateInfo(baseLayer); if (nxt.shortNameHash == shortHash) return true; } return false; } // Animation Events (optional) public void LeftFootOnFloor() { if (LeftFootDustPoof) { LeftFootDustPoof.Stop(); LeftFootDustPoof.Play(); } } public void RightFootOnFloor() { if (RightFootDustPoof) { RightFootDustPoof.Stop(); RightFootDustPoof.Play(); } } }