2026-09-29 16:11:54 +01:00
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.AI;
|
|
|
|
|
|
|
|
|
|
public enum NPCFaction
|
|
|
|
|
{
|
|
|
|
|
Friendly, // Never attacks
|
|
|
|
|
Neutral, // Only attacks if attacked first
|
|
|
|
|
Enemy // Attacks on sight
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RequireComponent(typeof(NavMeshAgent))]
|
|
|
|
|
public class NPCController : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
private NavMeshAgent _agent;
|
|
|
|
|
|
|
|
|
|
[Tooltip("Leave blank to automatically find the Animator in child objects")]
|
|
|
|
|
[SerializeField] private Animator _animator;
|
|
|
|
|
|
|
|
|
|
[Header("NPC Configuration")]
|
|
|
|
|
public NPCData npcData;
|
|
|
|
|
|
|
|
|
|
[Tooltip("The socket on the character model where the weapon should spawn (e.g., Prop_R). Leave blank to auto-search.")]
|
|
|
|
|
public Transform weaponSocket;
|
|
|
|
|
|
2026-09-30 16:39:18 +01:00
|
|
|
[Header("Ragdoll Settings")]
|
|
|
|
|
[Tooltip("If true, the NPC will collapse into a ragdoll on death rather than playing an animation.")]
|
|
|
|
|
public bool useRagdoll = true;
|
|
|
|
|
|
|
|
|
|
private Rigidbody[] _boneRigidbodies;
|
|
|
|
|
|
2026-09-29 16:11:54 +01:00
|
|
|
[Header("Patrol Testing")]
|
|
|
|
|
[Tooltip("If true, the NPC will automatically walk between the patrol points below.")]
|
|
|
|
|
public bool isPatrolling = false;
|
|
|
|
|
[Tooltip("Drag empty GameObjects here to act as waypoints.")]
|
|
|
|
|
public Transform[] patrolPoints;
|
|
|
|
|
[Tooltip("How many seconds to wait at each point before moving to the next one.")]
|
|
|
|
|
public float waitTimeAtPoint = 2.0f;
|
|
|
|
|
|
|
|
|
|
[Header("Runtime State")]
|
|
|
|
|
public float currentHealth;
|
|
|
|
|
public NPCFaction currentFaction;
|
|
|
|
|
|
|
|
|
|
[Tooltip("If assigned, the NPC will abandon patrol to chase and attack this target.")]
|
|
|
|
|
public Transform combatTarget;
|
|
|
|
|
|
|
|
|
|
private int _currentPatrolIndex = 0;
|
|
|
|
|
private float _patrolWaitTimer = 0f;
|
|
|
|
|
private float _lastAttackTime = 0f;
|
|
|
|
|
private float _stunTimer = 0f;
|
|
|
|
|
private bool _isDead = false;
|
2026-09-30 16:39:18 +01:00
|
|
|
private bool _isBlocking = false;
|
|
|
|
|
private float _lastBlockAttemptTime = 0f;
|
2026-09-29 16:11:54 +01:00
|
|
|
private GameObject _equippedWeaponInstance;
|
|
|
|
|
|
|
|
|
|
// Animator Hashes specifically for Synty's AC_Sidekick and AC_Polygon controllers
|
|
|
|
|
private readonly int _movementInputHeldHash = Animator.StringToHash("MovementInputHeld");
|
|
|
|
|
private readonly int _movementInputPressedHash = Animator.StringToHash("MovementInputPressed");
|
|
|
|
|
private readonly int _moveSpeedHash = Animator.StringToHash("MoveSpeed");
|
|
|
|
|
private readonly int _currentGaitHash = Animator.StringToHash("CurrentGait");
|
|
|
|
|
private readonly int _isGroundedHash = Animator.StringToHash("IsGrounded");
|
|
|
|
|
private readonly int _isWalkingHash = Animator.StringToHash("IsWalking");
|
|
|
|
|
private readonly int _isStoppedHash = Animator.StringToHash("IsStopped");
|
|
|
|
|
|
|
|
|
|
private bool _wasMovingLastFrame = false;
|
|
|
|
|
|
|
|
|
|
// Additional Animator Hashes for Combat
|
|
|
|
|
private readonly int _attackHash = Animator.StringToHash("Attack");
|
|
|
|
|
private readonly int _takeDamageHash = Animator.StringToHash("TakeDamage");
|
|
|
|
|
private readonly int _deathHash = Animator.StringToHash("Death"); // Optional
|
2026-09-30 16:39:18 +01:00
|
|
|
private readonly int _isBlockingHash = Animator.StringToHash("IsBlocking");
|
2026-09-29 16:11:54 +01:00
|
|
|
|
|
|
|
|
private void Start()
|
|
|
|
|
{
|
|
|
|
|
_agent = GetComponent<NavMeshAgent>();
|
|
|
|
|
|
|
|
|
|
if (_animator == null)
|
|
|
|
|
{
|
|
|
|
|
_animator = GetComponentInChildren<Animator>();
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 16:39:18 +01:00
|
|
|
// Gather all rigidbodies that belong to the ragdoll
|
|
|
|
|
_boneRigidbodies = GetComponentsInChildren<Rigidbody>();
|
|
|
|
|
SetRagdollState(false); // Make them kinematic while alive
|
|
|
|
|
|
2026-09-29 16:11:54 +01:00
|
|
|
// Auto-assign player as combat target if empty (great for quick testing!)
|
|
|
|
|
if (combatTarget == null)
|
|
|
|
|
{
|
|
|
|
|
PlayerStats p = FindObjectOfType<PlayerStats>();
|
|
|
|
|
if (p != null) combatTarget = p.transform;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (npcData != null)
|
|
|
|
|
{
|
|
|
|
|
currentHealth = npcData.maxHealth;
|
|
|
|
|
currentFaction = npcData.defaultFaction;
|
|
|
|
|
EquipWeapon();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning($"NPC {gameObject.name} is missing an NPCData object!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Let the NavMeshAgent natively handle where the NPC is facing and moving
|
|
|
|
|
_agent.updatePosition = true;
|
|
|
|
|
_agent.updateRotation = true;
|
|
|
|
|
|
|
|
|
|
if (isPatrolling && patrolPoints.Length > 0)
|
|
|
|
|
{
|
|
|
|
|
WalkTo(patrolPoints[_currentPatrolIndex].position);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EquipWeapon()
|
|
|
|
|
{
|
|
|
|
|
if (npcData.weaponPrefab == null) return;
|
|
|
|
|
|
|
|
|
|
// Try to automatically find a Prop_R socket if not assigned
|
|
|
|
|
if (weaponSocket == null)
|
|
|
|
|
{
|
|
|
|
|
// Synty characters often have a Prop_R under the right hand
|
|
|
|
|
Transform[] allChildren = GetComponentsInChildren<Transform>();
|
|
|
|
|
foreach (Transform t in allChildren)
|
|
|
|
|
{
|
|
|
|
|
if (t.name == "Prop_R" || t.name == "Weapon_R")
|
|
|
|
|
{
|
|
|
|
|
weaponSocket = t;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (weaponSocket != null)
|
|
|
|
|
{
|
|
|
|
|
_equippedWeaponInstance = Instantiate(npcData.weaponPrefab, weaponSocket);
|
|
|
|
|
|
|
|
|
|
// If the weapon prefab has a Weapon script on it, we might want to apply offsets
|
|
|
|
|
Weapon weaponComp = _equippedWeaponInstance.GetComponent<Weapon>();
|
|
|
|
|
if (weaponComp != null)
|
|
|
|
|
{
|
|
|
|
|
_equippedWeaponInstance.transform.localPosition = weaponComp.equippedPositionOffset;
|
|
|
|
|
_equippedWeaponInstance.transform.localRotation = Quaternion.Euler(weaponComp.equippedRotationOffset);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_equippedWeaponInstance.transform.localPosition = Vector3.zero;
|
|
|
|
|
_equippedWeaponInstance.transform.localRotation = Quaternion.identity;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 16:39:18 +01:00
|
|
|
private void SetRagdollState(bool isActive)
|
|
|
|
|
{
|
|
|
|
|
if (_boneRigidbodies == null) return;
|
|
|
|
|
|
|
|
|
|
foreach (Rigidbody rb in _boneRigidbodies)
|
|
|
|
|
{
|
|
|
|
|
// We ignore the root GameObject's Rigidbody if it has one
|
|
|
|
|
if (rb.gameObject == gameObject) continue;
|
|
|
|
|
|
|
|
|
|
rb.isKinematic = !isActive;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 16:11:54 +01:00
|
|
|
private void Update()
|
|
|
|
|
{
|
|
|
|
|
if (_isDead) return;
|
|
|
|
|
|
|
|
|
|
// Handle Stun / Hit Reaction
|
|
|
|
|
if (_stunTimer > 0f)
|
|
|
|
|
{
|
|
|
|
|
_stunTimer -= Time.deltaTime;
|
|
|
|
|
if (_agent != null && _agent.isOnNavMesh) _agent.isStopped = true;
|
|
|
|
|
UpdateAnimatorLocomotion();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Combat Override (Only if they are an enemy)
|
|
|
|
|
bool shouldAggro = combatTarget != null && currentFaction == NPCFaction.Enemy;
|
|
|
|
|
|
|
|
|
|
if (shouldAggro && Vector3.Distance(transform.position, combatTarget.position) <= npcData.aggroRadius)
|
|
|
|
|
{
|
|
|
|
|
UpdateCombat();
|
|
|
|
|
}
|
|
|
|
|
else if (isPatrolling)
|
|
|
|
|
{
|
|
|
|
|
UpdatePatrol();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// If neither patrolling nor fighting, just stop
|
|
|
|
|
if (_agent != null && _agent.isOnNavMesh && !_agent.isStopped)
|
|
|
|
|
{
|
|
|
|
|
_agent.isStopped = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UpdateAnimatorLocomotion();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateCombat()
|
|
|
|
|
{
|
|
|
|
|
if (combatTarget == null || _agent == null || !_agent.isOnNavMesh) return;
|
|
|
|
|
|
|
|
|
|
float distToTarget = Vector3.Distance(transform.position, combatTarget.position);
|
|
|
|
|
|
2026-09-30 16:39:18 +01:00
|
|
|
PlayerStats playerStats = combatTarget.GetComponent<PlayerStats>();
|
|
|
|
|
MeleeCombatLayerController playerCombat = combatTarget.GetComponent<MeleeCombatLayerController>();
|
|
|
|
|
|
|
|
|
|
bool isPlayerBlocking = playerStats != null && playerStats.isBlocking;
|
|
|
|
|
bool isPlayerAttacking = playerCombat != null && playerCombat.IsAttacking;
|
|
|
|
|
|
2026-09-29 16:11:54 +01:00
|
|
|
if (distToTarget <= npcData.attackRadius)
|
|
|
|
|
{
|
2026-09-30 16:39:18 +01:00
|
|
|
// Stop and face them
|
2026-09-29 16:11:54 +01:00
|
|
|
_agent.isStopped = true;
|
|
|
|
|
Vector3 direction = (combatTarget.position - transform.position).normalized;
|
|
|
|
|
direction.y = 0;
|
|
|
|
|
if (direction != Vector3.zero)
|
|
|
|
|
{
|
|
|
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 5f);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 16:39:18 +01:00
|
|
|
// Blocking Logic: Try to block if the player is attacking
|
|
|
|
|
if (isPlayerAttacking && !_isBlocking && Time.time >= _lastBlockAttemptTime + 1.5f)
|
2026-09-29 16:11:54 +01:00
|
|
|
{
|
2026-09-30 16:39:18 +01:00
|
|
|
_lastBlockAttemptTime = Time.time;
|
|
|
|
|
if (Random.value <= npcData.blockChance)
|
|
|
|
|
{
|
|
|
|
|
SetBlockState(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop blocking if the player stops attacking (or after a small delay)
|
|
|
|
|
if (!isPlayerAttacking && _isBlocking && Time.time >= _lastBlockAttemptTime + 1.0f)
|
|
|
|
|
{
|
|
|
|
|
SetBlockState(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attacking Logic: Only attack if we are NOT currently blocking
|
|
|
|
|
if (!_isBlocking)
|
|
|
|
|
{
|
|
|
|
|
// If the player is turtling (blocking), attack twice as fast to try and break their guard!
|
|
|
|
|
float currentCooldown = isPlayerBlocking ? npcData.attackCooldown * 0.5f : npcData.attackCooldown;
|
|
|
|
|
|
|
|
|
|
if (Time.time >= _lastAttackTime + currentCooldown)
|
|
|
|
|
{
|
|
|
|
|
PerformAttack();
|
|
|
|
|
}
|
2026-09-29 16:11:54 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-09-30 16:39:18 +01:00
|
|
|
// Target is too far, chase them! (And stop blocking if we were)
|
|
|
|
|
if (_isBlocking) SetBlockState(false);
|
2026-09-29 16:11:54 +01:00
|
|
|
RunTo(combatTarget.position);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 16:39:18 +01:00
|
|
|
private void SetBlockState(bool block)
|
|
|
|
|
{
|
|
|
|
|
_isBlocking = block;
|
|
|
|
|
if (_animator != null)
|
|
|
|
|
{
|
|
|
|
|
_animator.SetBool(_isBlockingHash, block);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 16:11:54 +01:00
|
|
|
private void PerformAttack()
|
|
|
|
|
{
|
|
|
|
|
_lastAttackTime = Time.time;
|
|
|
|
|
if (_animator != null)
|
|
|
|
|
{
|
|
|
|
|
_animator.SetTrigger(_attackHash);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 16:39:18 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Some Synty animations use "DealDamage" instead of "Hit" as the event name.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void DealDamage()
|
|
|
|
|
{
|
|
|
|
|
Hit();
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 16:11:54 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Call this from the "Hit" Animation Event on the NPC's attack animation
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void Hit()
|
|
|
|
|
{
|
|
|
|
|
if (_isDead || combatTarget == null) return;
|
|
|
|
|
|
|
|
|
|
// If the target is still within attack range when the animation hits, deal damage
|
|
|
|
|
if (Vector3.Distance(transform.position, combatTarget.position) <= npcData.attackRadius + 0.5f)
|
|
|
|
|
{
|
2026-09-30 16:39:18 +01:00
|
|
|
float damageToDeal = npcData.weaponData != null ? npcData.weaponData.damage : npcData.baseDamage;
|
|
|
|
|
Debug.Log($"NPC landed a hit on the Player for {damageToDeal} damage!");
|
|
|
|
|
|
|
|
|
|
// Try to hit the combat controller first so it can process blocks/stamina/animations
|
|
|
|
|
MeleeCombatLayerController playerCombat = combatTarget.GetComponent<MeleeCombatLayerController>();
|
|
|
|
|
if (playerCombat != null)
|
2026-09-29 16:11:54 +01:00
|
|
|
{
|
2026-09-30 16:39:18 +01:00
|
|
|
playerCombat.TakeDamage(damageToDeal);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
PlayerStats playerStats = combatTarget.GetComponent<PlayerStats>();
|
|
|
|
|
if (playerStats != null) playerStats.TakeDamage(damageToDeal);
|
2026-09-29 16:11:54 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void TakeDamage(float amount)
|
|
|
|
|
{
|
|
|
|
|
if (_isDead) return;
|
|
|
|
|
|
2026-09-30 16:39:18 +01:00
|
|
|
if (_isBlocking)
|
|
|
|
|
{
|
|
|
|
|
float blockedAmount = amount * npcData.blockReductionPercentage;
|
|
|
|
|
amount -= blockedAmount;
|
|
|
|
|
Debug.Log($"NPC Blocked {blockedAmount} damage! Taking only {amount} damage.");
|
|
|
|
|
|
|
|
|
|
// Optionally: Break guard if hit hard?
|
|
|
|
|
// SetBlockState(false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 16:11:54 +01:00
|
|
|
currentHealth -= amount;
|
|
|
|
|
Debug.Log($"NPC took {amount} damage! Health remaining: {currentHealth}");
|
|
|
|
|
|
|
|
|
|
// If a neutral NPC gets hit, they become hostile!
|
|
|
|
|
if (currentFaction == NPCFaction.Neutral)
|
|
|
|
|
{
|
|
|
|
|
currentFaction = NPCFaction.Enemy;
|
|
|
|
|
Debug.Log("Neutral NPC was attacked and is now hostile!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentHealth <= 0)
|
|
|
|
|
{
|
|
|
|
|
Die();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (_animator != null)
|
|
|
|
|
{
|
|
|
|
|
_animator.SetTrigger(_takeDamageHash);
|
|
|
|
|
// Stun them for 1 second so they don't slide while playing the hit animation
|
|
|
|
|
_stunTimer = 1.0f;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Die()
|
|
|
|
|
{
|
|
|
|
|
_isDead = true;
|
|
|
|
|
|
|
|
|
|
if (_agent != null && _agent.isOnNavMesh)
|
|
|
|
|
{
|
|
|
|
|
_agent.isStopped = true;
|
2026-09-30 16:39:18 +01:00
|
|
|
_agent.enabled = false; // Turn off the agent completely so physics don't fight it
|
2026-09-29 16:11:54 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-30 16:39:18 +01:00
|
|
|
if (useRagdoll)
|
2026-09-29 16:11:54 +01:00
|
|
|
{
|
2026-09-30 16:39:18 +01:00
|
|
|
if (_animator != null) _animator.enabled = false; // Turn off animator so physics take over
|
|
|
|
|
SetRagdollState(true); // Let the limbs fall!
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (_animator != null)
|
|
|
|
|
{
|
|
|
|
|
_animator.SetTrigger(_deathHash);
|
|
|
|
|
}
|
2026-09-29 16:11:54 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-30 16:39:18 +01:00
|
|
|
// Disable ALL root colliders on this object (capsules) so it doesn't block the player
|
2026-09-29 16:11:54 +01:00
|
|
|
Collider[] cols = GetComponents<Collider>();
|
|
|
|
|
foreach(Collider col in cols)
|
|
|
|
|
{
|
|
|
|
|
col.enabled = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 16:39:18 +01:00
|
|
|
// Find and disable the Lock-On script even if it's on a child object
|
|
|
|
|
MonoBehaviour lockOnScript = null;
|
|
|
|
|
foreach (MonoBehaviour mb in GetComponentsInChildren<MonoBehaviour>())
|
|
|
|
|
{
|
|
|
|
|
if (mb != null && mb.GetType().Name == "SampleObjectLockOn")
|
|
|
|
|
{
|
|
|
|
|
lockOnScript = mb;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-29 16:11:54 +01:00
|
|
|
if (lockOnScript != null)
|
|
|
|
|
{
|
|
|
|
|
lockOnScript.enabled = false;
|
2026-09-30 16:39:18 +01:00
|
|
|
// Also disable its collider so we can't lock on anymore!
|
|
|
|
|
Collider lockOnCol = lockOnScript.GetComponent<Collider>();
|
|
|
|
|
if (lockOnCol != null) lockOnCol.enabled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Just to be absolutely safe, let's disable ALL trigger colliders in the children
|
|
|
|
|
// (This catches the giant lock-on sphere without disabling the physical ragdoll bone colliders!)
|
|
|
|
|
Collider[] allCols = GetComponentsInChildren<Collider>();
|
|
|
|
|
foreach (Collider c in allCols)
|
|
|
|
|
{
|
|
|
|
|
if (c.isTrigger) c.enabled = false;
|
2026-09-29 16:11:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Debug.Log("NPC has died!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateAnimatorLocomotion()
|
|
|
|
|
{
|
|
|
|
|
if (_animator == null) return;
|
|
|
|
|
|
|
|
|
|
// 1. Is the NPC currently trying to move somewhere?
|
|
|
|
|
bool isMoving = !_agent.isStopped && _agent.hasPath && _agent.remainingDistance > 0.1f && _agent.velocity.sqrMagnitude > 0.01f;
|
|
|
|
|
|
|
|
|
|
// 2. We must feed the NavMeshAgent's movement state into Synty's Animation variables
|
|
|
|
|
if (isMoving)
|
|
|
|
|
{
|
|
|
|
|
_animator.SetBool(_movementInputHeldHash, true);
|
|
|
|
|
_animator.SetBool(_isStoppedHash, false);
|
|
|
|
|
_animator.SetBool(_isWalkingHash, true);
|
|
|
|
|
|
|
|
|
|
if (!_wasMovingLastFrame)
|
|
|
|
|
{
|
|
|
|
|
_animator.SetBool(_movementInputPressedHash, true);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_animator.SetBool(_movementInputPressedHash, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float currentSpeed = _agent.velocity.magnitude;
|
|
|
|
|
_animator.SetFloat(_moveSpeedHash, currentSpeed);
|
|
|
|
|
|
|
|
|
|
if (currentSpeed > npcData.walkSpeed + 0.5f)
|
|
|
|
|
{
|
|
|
|
|
_animator.SetInteger(_currentGaitHash, 2); // Run
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_animator.SetInteger(_currentGaitHash, 1); // Walk
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_animator.SetBool(_movementInputHeldHash, false);
|
|
|
|
|
_animator.SetBool(_movementInputPressedHash, false);
|
|
|
|
|
_animator.SetBool(_isStoppedHash, true);
|
|
|
|
|
_animator.SetBool(_isWalkingHash, false);
|
|
|
|
|
|
|
|
|
|
_animator.SetFloat(_moveSpeedHash, 0f);
|
|
|
|
|
_animator.SetInteger(_currentGaitHash, 0); // Idle
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_animator.SetBool(_isGroundedHash, true);
|
|
|
|
|
_wasMovingLastFrame = isMoving;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdatePatrol()
|
|
|
|
|
{
|
|
|
|
|
if (patrolPoints == null || patrolPoints.Length == 0 || _agent == null || !_agent.isOnNavMesh) return;
|
|
|
|
|
|
|
|
|
|
// Check if we have arrived at our destination
|
|
|
|
|
if (!_agent.pathPending && _agent.remainingDistance <= _agent.stoppingDistance + 0.1f)
|
|
|
|
|
{
|
|
|
|
|
_patrolWaitTimer += Time.deltaTime;
|
|
|
|
|
|
|
|
|
|
// Once we've waited long enough, pick the next point
|
|
|
|
|
if (_patrolWaitTimer >= waitTimeAtPoint)
|
|
|
|
|
{
|
|
|
|
|
_patrolWaitTimer = 0f;
|
|
|
|
|
_currentPatrolIndex = (_currentPatrolIndex + 1) % patrolPoints.Length;
|
|
|
|
|
WalkTo(patrolPoints[_currentPatrolIndex].position);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Helper method: Call this from other scripts to make the NPC walk to a target!
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void WalkTo(Vector3 destination)
|
|
|
|
|
{
|
|
|
|
|
if (_agent == null || !_agent.isOnNavMesh) return;
|
|
|
|
|
_agent.speed = npcData.walkSpeed;
|
|
|
|
|
_agent.isStopped = false;
|
|
|
|
|
_agent.SetDestination(destination);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Helper method: Call this from other scripts to make the NPC run to a target!
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void RunTo(Vector3 destination)
|
|
|
|
|
{
|
|
|
|
|
if (_agent == null || !_agent.isOnNavMesh) return;
|
|
|
|
|
_agent.speed = npcData.runSpeed;
|
|
|
|
|
_agent.isStopped = false;
|
|
|
|
|
_agent.SetDestination(destination);
|
|
|
|
|
}
|
2026-09-30 16:39:18 +01:00
|
|
|
|
|
|
|
|
#region Animation Event Dummies
|
|
|
|
|
// These methods are here purely to absorb the Animation Events you added for the Player's Combo System.
|
|
|
|
|
// Since NPCs use the exact same animation clips, they will fire the events too!
|
|
|
|
|
// This stops Unity from throwing "has no receiver" errors.
|
|
|
|
|
public void OpenComboWindow()
|
|
|
|
|
{
|
|
|
|
|
// NPCs don't have a UI combo system, so they just ignore this!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void CloseComboWindow()
|
|
|
|
|
{
|
|
|
|
|
// Ignored!
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
2026-09-29 16:11:54 +01:00
|
|
|
}
|