175 lines
5.3 KiB
C#
175 lines
5.3 KiB
C#
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.AI;
|
||
|
|
|
||
|
|
[RequireComponent(typeof(CharacterStats))]
|
||
|
|
[RequireComponent(typeof(CombatController))]
|
||
|
|
public class CombatAI : MonoBehaviour
|
||
|
|
{
|
||
|
|
[Header("Aggro & Retaliation")]
|
||
|
|
[Tooltip("If true, automatically attacks target when taking damage.")]
|
||
|
|
[SerializeField] private bool autoRetaliate = true;
|
||
|
|
|
||
|
|
[Tooltip("Detection distance for hostile eyesight vision.")]
|
||
|
|
[SerializeField] private float eyesightRange = 8.0f;
|
||
|
|
|
||
|
|
[Tooltip("Layer mask for targeting (default Player).")]
|
||
|
|
[SerializeField] private LayerMask targetLayer = ~0;
|
||
|
|
|
||
|
|
[Header("Movement")]
|
||
|
|
[SerializeField] private float chaseSpeed = 3.5f;
|
||
|
|
|
||
|
|
private CharacterStats stats;
|
||
|
|
private CombatController combat;
|
||
|
|
private NavMeshAgent navAgent;
|
||
|
|
private NPC npcComponent;
|
||
|
|
|
||
|
|
private void Awake()
|
||
|
|
{
|
||
|
|
stats = GetComponent<CharacterStats>();
|
||
|
|
combat = GetComponent<CombatController>();
|
||
|
|
navAgent = GetComponent<NavMeshAgent>();
|
||
|
|
npcComponent = GetComponent<NPC>();
|
||
|
|
|
||
|
|
stats.onDeath.AddListener(OnDeath);
|
||
|
|
stats.onHealthChanged.AddListener(OnHealthChanged);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void Update()
|
||
|
|
{
|
||
|
|
if (stats.IsDead) return;
|
||
|
|
|
||
|
|
bool isHostile = (npcComponent != null)
|
||
|
|
? npcComponent.Relationship == NPCRelationship.Hostile
|
||
|
|
: false;
|
||
|
|
|
||
|
|
// Hostile eyesight check if no active target
|
||
|
|
if (isHostile && combat.CurrentTarget == null)
|
||
|
|
{
|
||
|
|
ScanForHostileTarget();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Process combat chase & attack
|
||
|
|
if (combat.CurrentTarget != null)
|
||
|
|
{
|
||
|
|
if (combat.CurrentTarget.IsDead)
|
||
|
|
{
|
||
|
|
combat.ClearTarget();
|
||
|
|
StopMovement();
|
||
|
|
SetNPCState(NPCState.Idle);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set NPC state to Attacking to stop wandering
|
||
|
|
SetNPCState(NPCState.Attacking);
|
||
|
|
|
||
|
|
float distanceToTarget = Vector3.Distance(transform.position, combat.CurrentTarget.transform.position);
|
||
|
|
|
||
|
|
if (distanceToTarget > stats.AttackRange)
|
||
|
|
{
|
||
|
|
// Chase target
|
||
|
|
MoveToPosition(combat.CurrentTarget.transform.position);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// In attack range
|
||
|
|
StopMovement();
|
||
|
|
combat.TryExecuteAttack();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void SetNPCState(NPCState newState)
|
||
|
|
{
|
||
|
|
if (npcComponent != null && npcComponent.CurrentState != newState)
|
||
|
|
{
|
||
|
|
npcComponent.CurrentState = newState;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ScanForHostileTarget()
|
||
|
|
{
|
||
|
|
Collider[] hits = Physics.OverlapSphere(transform.position, eyesightRange, targetLayer);
|
||
|
|
foreach (var hit in hits)
|
||
|
|
{
|
||
|
|
if (hit.CompareTag("Player"))
|
||
|
|
{
|
||
|
|
CharacterStats playerStats = hit.GetComponent<CharacterStats>();
|
||
|
|
if (playerStats != null && !playerStats.IsDead)
|
||
|
|
{
|
||
|
|
combat.SetTarget(playerStats);
|
||
|
|
Debug.Log($"[{gameObject.name}] Aggroed on Player '{playerStats.CharacterName}' via Eyesight Range!");
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnHealthChanged(float currentHealth, float maxHealth)
|
||
|
|
{
|
||
|
|
// Auto Retaliation on being attacked
|
||
|
|
if (autoRetaliate && combat.CurrentTarget == null)
|
||
|
|
{
|
||
|
|
// Find attacker by scanning nearby player or closest damage dealer
|
||
|
|
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
||
|
|
if (playerObj != null)
|
||
|
|
{
|
||
|
|
CharacterStats playerStats = playerObj.GetComponent<CharacterStats>();
|
||
|
|
if (playerStats != null && !playerStats.IsDead)
|
||
|
|
{
|
||
|
|
combat.SetTarget(playerStats);
|
||
|
|
Debug.Log($"[{gameObject.name}] Auto-retaliating against '{playerStats.CharacterName}'!");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void MoveToPosition(Vector3 position)
|
||
|
|
{
|
||
|
|
if (navAgent != null && navAgent.enabled)
|
||
|
|
{
|
||
|
|
navAgent.speed = chaseSpeed;
|
||
|
|
navAgent.stoppingDistance = stats.AttackRange * 0.8f;
|
||
|
|
navAgent.SetDestination(position);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Vector3 dir = (position - transform.position);
|
||
|
|
dir.y = 0;
|
||
|
|
if (dir.magnitude > stats.AttackRange)
|
||
|
|
{
|
||
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10f * Time.deltaTime);
|
||
|
|
transform.position += dir.normalized * chaseSpeed * Time.deltaTime;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void StopMovement()
|
||
|
|
{
|
||
|
|
if (navAgent != null && navAgent.enabled)
|
||
|
|
{
|
||
|
|
if (navAgent.hasPath) navAgent.ResetPath();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnDeath(GameObject killer)
|
||
|
|
{
|
||
|
|
combat.ClearTarget();
|
||
|
|
StopMovement();
|
||
|
|
SetNPCState(NPCState.Dead);
|
||
|
|
enabled = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnDrawGizmosSelected()
|
||
|
|
{
|
||
|
|
bool isHostileGizmo = (npcComponent != null)
|
||
|
|
? npcComponent.Relationship == NPCRelationship.Hostile
|
||
|
|
: false;
|
||
|
|
|
||
|
|
if (isHostileGizmo)
|
||
|
|
{
|
||
|
|
Gizmos.color = Color.red;
|
||
|
|
Gizmos.DrawWireSphere(transform.position, eyesightRange);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|