2025-08-18 17:52:40 +05:00
|
|
|
|
using UnityEngine;
|
2025-09-11 18:46:20 +05:00
|
|
|
|
using System.Collections;
|
2025-08-18 17:52:40 +05:00
|
|
|
|
|
|
|
|
|
public class ChaseRunEnemy : MonoBehaviour
|
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
[Header("Refs")]
|
2025-08-18 17:52:40 +05:00
|
|
|
|
public Animator m_animator;
|
|
|
|
|
public Rigidbody rb;
|
2025-09-11 18:46:20 +05:00
|
|
|
|
|
|
|
|
|
[Header("Movement")]
|
|
|
|
|
public float moveSpeed = 4f;
|
|
|
|
|
[Tooltip("Enter stomp when within this distance to player.")]
|
2025-08-18 17:52:40 +05:00
|
|
|
|
public float stopDistance = 1.5f;
|
2025-09-11 18:46:20 +05:00
|
|
|
|
[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<73>t enter.")]
|
|
|
|
|
public string stompStateName = ""; // e.g. "Stomp" or "Squish"
|
|
|
|
|
[Tooltip("Animator base layer index.")]
|
|
|
|
|
public int baseLayer = 0;
|
2025-08-18 17:52:40 +05:00
|
|
|
|
|
2025-09-04 17:01:33 +05:00
|
|
|
|
[Header("Speed Source")]
|
2025-09-11 18:46:20 +05:00
|
|
|
|
public bool usePlayerSpeed = false; // keep false so enemy doesn't inherit wolf boost
|
2025-08-18 17:52:40 +05:00
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
[Header("VFX (optional)")]
|
2025-09-04 18:10:32 +05:00
|
|
|
|
[SerializeField] private ParticleSystem LeftFootDustPoof, RightFootDustPoof;
|
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
[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;
|
|
|
|
|
|
2025-08-18 17:52:40 +05:00
|
|
|
|
private void Awake()
|
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
if (!rb) rb = GetComponent<Rigidbody>();
|
2025-08-18 17:52:40 +05:00
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
var playerObj = GameObject.FindGameObjectWithTag("Player");
|
|
|
|
|
if (playerObj)
|
2025-08-18 17:52:40 +05:00
|
|
|
|
{
|
|
|
|
|
player = playerObj.transform;
|
2025-09-11 18:46:20 +05:00
|
|
|
|
playerController = player.GetComponentInParent<ChasePlayerController>();
|
2025-08-18 17:52:40 +05:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError("[ChaseRunEnemy] Player not found in scene.");
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
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.");
|
2025-08-18 17:52:40 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnEnable()
|
|
|
|
|
{
|
2025-09-04 17:01:33 +05:00
|
|
|
|
if (usePlayerSpeed)
|
|
|
|
|
ChasePlayerController.OnMoveSpeedChanged += UpdateMoveSpeed;
|
2025-08-18 17:52:40 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDisable()
|
|
|
|
|
{
|
2025-09-04 17:01:33 +05:00
|
|
|
|
if (usePlayerSpeed)
|
2025-09-11 18:46:20 +05:00
|
|
|
|
ChasePlayerController.OnMoveSpeedChanged -= UpdateMoveSpeed; // correct unsubscribe
|
2025-08-18 17:52:40 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
private void UpdateMoveSpeed(float speed) => moveSpeed = speed;
|
2025-08-18 17:52:40 +05:00
|
|
|
|
|
|
|
|
|
private void FixedUpdate()
|
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
if (!player) return;
|
2025-08-18 17:52:40 +05:00
|
|
|
|
|
|
|
|
|
Vector3 toPlayer = player.position - transform.position;
|
|
|
|
|
float distanceSqr = toPlayer.sqrMagnitude;
|
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
if (stompTriggered)
|
2025-08-18 17:52:40 +05:00
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
// Track closest approach while in stomp
|
|
|
|
|
if (distanceSqr < minDistanceSqrDuringStomp)
|
|
|
|
|
minDistanceSqrDuringStomp = distanceSqr;
|
|
|
|
|
|
|
|
|
|
// If player actually died, do nothing<6E>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)
|
2025-08-18 17:52:40 +05:00
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
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;
|
2025-08-18 17:52:40 +05:00
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
// Not stomping yet: check entry condition
|
|
|
|
|
if (distanceSqr <= stopDistanceSqr)
|
2025-08-18 17:52:40 +05:00
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
TriggerStomp(distanceSqr);
|
|
|
|
|
return;
|
2025-08-18 17:52:40 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
// Otherwise, keep running forward
|
2025-08-18 17:52:40 +05:00
|
|
|
|
MoveForward();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void MoveForward()
|
|
|
|
|
{
|
|
|
|
|
rb.MovePosition(rb.position + Vector3.back * moveSpeed * Time.fixedDeltaTime);
|
|
|
|
|
}
|
2025-09-04 18:10:32 +05:00
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
private void TriggerStomp(float currentDistanceSqr)
|
|
|
|
|
{
|
|
|
|
|
if (stompTriggered) return;
|
|
|
|
|
stompTriggered = true;
|
|
|
|
|
stompStartTime = Time.time;
|
|
|
|
|
minDistanceSqrDuringStomp = currentDistanceSqr;
|
|
|
|
|
|
|
|
|
|
// Stop instantly via physics <20> 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 <20> 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)
|
2025-09-04 18:10:32 +05:00
|
|
|
|
public void LeftFootOnFloor()
|
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
if (LeftFootDustPoof) { LeftFootDustPoof.Stop(); LeftFootDustPoof.Play(); }
|
2025-09-04 18:10:32 +05:00
|
|
|
|
}
|
2025-09-11 18:46:20 +05:00
|
|
|
|
|
2025-09-04 18:10:32 +05:00
|
|
|
|
public void RightFootOnFloor()
|
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
if (RightFootDustPoof) { RightFootDustPoof.Stop(); RightFootDustPoof.Play(); }
|
2025-09-04 18:10:32 +05:00
|
|
|
|
}
|
2025-08-18 17:52:40 +05:00
|
|
|
|
}
|