284 lines
9.7 KiB
C#
284 lines
9.7 KiB
C#
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<Rigidbody>();
|
||
|
||
var playerObj = GameObject.FindGameObjectWithTag("Player");
|
||
if (playerObj)
|
||
{
|
||
player = playerObj.transform;
|
||
playerController = player.GetComponentInParent<ChasePlayerController>();
|
||
}
|
||
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(); }
|
||
}
|
||
}
|