using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using UnityEngine.Events; public class NPCControllerBase : MonoBehaviour { [Header("Settings:")] [SerializeField] protected bool maintainsDistance = false; [SerializeField] protected float projectileRange; [SerializeField] protected float meleeRange; [SerializeField] protected float distanceToChangePatrolDestination; [SerializeField] protected float patrolAgentSpeed; [SerializeField] protected float chasingAgentSpeed; [SerializeField] protected float timeBetweenAttacks = 1f; //change in editor public float ProjectileRange => projectileRange; public float MeleeRange => meleeRange; [HideInInspector] public Taggable myTag; [HideInInspector] public NPCAnimatorControllerBase animatorController; [HideInInspector] public NPCAbilityPriorityManager abilityPriorityManager; [HideInInspector] public AbilityCooldownTracker abilityCooldownTracker; protected NavMeshAgent agent; public List possibleTargets = new List(); public Taggable currentTarget; protected BaseAbility ability; protected float targetDistance = float.MaxValue; protected float distance; protected Taggable resultTarget; protected Vector3 patrolDestination = new Vector3(); protected float counter = 0f; protected Health health; protected Mana mana; public Health Health => health; public Mana Mana => mana; protected bool isDead = false; protected bool waitingForAttackAnimation = false; protected Health possibleTargetHealth; public UnityEvent onPossibleTargetEnteredSight = new UnityEvent(); public UnityEvent onPossibleTargetExitedSight = new UnityEvent(); protected float thinkingRate = 10f; // Times per second (e.g., 4 = every 0.25s) protected float thinkingTimer = 0f; protected virtual void Awake() { myTag = GetComponentInParent(); agent = GetComponentInParent(); health = GetComponent(); mana = GetComponent(); abilityPriorityManager = GetComponentInChildren(); abilityCooldownTracker = GetComponentInChildren(); animatorController = GetComponentInChildren(); onPossibleTargetEnteredSight.AddListener(OnNewTargetIdentified); onPossibleTargetExitedSight.AddListener(OnPossibleExistingTargetLost); } protected virtual void Start() { isDead = false; counter = timeBetweenAttacks / 2f; health.onDeath.AddListener(OnDeath); } protected virtual void Update() { if (isDead) return; thinkingTimer += Time.deltaTime; counter += Time.deltaTime; if (thinkingTimer >= 1f / thinkingRate) { thinkingTimer = 0f; // reset if (HasTarget()) { ChasingUpdate(); } else { PatrollingUpdate(); } } } #region Checks /// /// /// possibleTargets.Count > 0 public virtual bool HasAvailableTargets() { for (int i = possibleTargets.Count - 1; i >= 0; i--) { if (possibleTargets[i] == null) possibleTargets.RemoveAt(i); } return possibleTargets.Count > 0; } /// /// /// currentTarget != null public virtual bool HasTarget() { return currentTarget != null; } /// /// /// possibleTargets.Contains(currentTarget) public virtual bool HasVisionOfCurrentTarget() { return possibleTargets.Contains(currentTarget); } /// /// /// agent.destination != null public virtual bool HasDestination() { return agent.destination != null; } /// /// /// /// /// Distance between agent.transform.position and positionCheck less than distanceCheck public virtual bool IsCloseEnough(Vector3 positionCheck, float distanceCheck) { return Vector3.Distance(agent.transform.position, positionCheck) <= distanceCheck; } /// /// /// counter >= timeBetweenAttacks public virtual bool IsReadyToAttack() { return counter >= timeBetweenAttacks; } #endregion protected virtual void ResetCounterOnAttackPerformed() { counter = 0; } protected virtual Taggable GetClosestTarget() { targetDistance = float.MaxValue; resultTarget = null; for (int i = possibleTargets.Count - 1; i >= 0; i--) { if (possibleTargets[i] == null) possibleTargets.RemoveAt(i); } //Debug.Log("CLOSEST COUNT: " + possibleTargets.Count); for (int i = 0; i < possibleTargets.Count; i++) { possibleTargetHealth = possibleTargets[i].GetComponent(); if (possibleTargetHealth.GetCurrentValue() <= 0) continue; distance = Vector3.Distance(possibleTargets[i].transform.position, agent.transform.position); if (distance < targetDistance) { targetDistance = distance; resultTarget = possibleTargets[i]; } } return resultTarget; } protected virtual Taggable GetFurthestAwayTarget() { targetDistance = 0f; resultTarget = null; for (int i = possibleTargets.Count - 1; i >= 0; i--) { if (possibleTargets[i] == null) possibleTargets.RemoveAt(i); } for (int i = 0; i < possibleTargets.Count; i++) { possibleTargetHealth = possibleTargets[i].GetComponent(); if (possibleTargetHealth.GetCurrentValue() <= 0) continue; distance = Vector3.Distance(possibleTargets[i].transform.position, agent.transform.position); if (distance > targetDistance) { targetDistance = distance; resultTarget = possibleTargets[i]; } } return resultTarget; } protected virtual void UpdateCurrentTarget(Taggable target) { if (target == null) { PatrolNewPosition(); return; } currentTarget = target; SetupAgentStats(currentTarget.transform.position, true); } protected virtual void UpdatePatrolTarget(Vector3 destination) { SetupAgentStats(destination); } protected virtual void SetupAgentStats(Vector3 destination, bool chasing = false) { if (isDead) return; agent.speed = chasing ? chasingAgentSpeed : patrolAgentSpeed; patrolDestination = destination; patrolDestination.y = 0f; agent.SetDestination(patrolDestination); } protected virtual void SetAgentMoving(bool isMoving) { if (isDead) return; agent.isStopped = !isMoving; } protected virtual void OnNewTargetIdentified() { if (HasTarget()) { OnNewTargetIdentifiedAndHasTarget(); } else //no current target { OnNewTargetIdentifiedAndNoCurrentTarget(); } } protected virtual void OnPossibleExistingTargetLost() { if (HasTarget()) { if (HasVisionOfCurrentTarget()) //current target inside sight (possibleTargets list) { OnPossibleTargetLostAndHasTargetAndVision(); } else //current target outside sight (possibleTargets list) { OnPossibleTargetLostAndHasTargetButNoVision(); } } else //no current target { if (HasAvailableTargets()) { OnPossibleTargetLostAndHasNoCurrentTargetButHasAvailableTargets(); } else //no available targets in sight { OnPossibleTargetLostAndHasNoCurrentTargetAndNoAvailableTargets(); } } } protected virtual void OnNewTargetIdentifiedAndHasTarget() { //someone entered sight, npc already has a target } protected virtual void OnNewTargetIdentifiedAndNoCurrentTarget() { //someone entered sight, npc does not have a target yet } protected virtual void OnPossibleTargetLostAndHasTargetAndVision() { //someone exited sight, npc already has target and vision } protected virtual void OnPossibleTargetLostAndHasTargetButNoVision() { //someone exited sight, npc already has target but no vision of it (possibly his target was the one getting out of sight) currentTarget = null; } protected virtual void OnPossibleTargetLostAndHasNoCurrentTargetButHasAvailableTargets() { //someone exited sight, npc has no target yet, there are available targets to pick } protected virtual void OnPossibleTargetLostAndHasNoCurrentTargetAndNoAvailableTargets() { //someone exited sight, npc has no target yet, there are NO available targets } protected virtual void TryAttack() { } bool shouldMove = false; protected virtual void ChasingUpdate() { if (IsReadyToAttack()) { TryAttack(); } else { SetupAgentStats(currentTarget.transform.position, true); if (maintainsDistance) { if (agent.remainingDistance < projectileRange) { shouldMove = true; FindEscapeRoute(); } else shouldMove = true; } else { if (agent.remainingDistance > agent.stoppingDistance) shouldMove = false; else shouldMove = true; } SetAgentMoving(shouldMove); } } protected Vector3 npcPosition; protected Vector3 targetPosition; protected Vector3 directionAwayFromTarget; protected float randomAngle; protected Vector3 escapeDirection; protected Vector3 desiredPosition; protected NavMeshHit hitEscapeRoute; protected float fallbackAngle; protected Vector3 fallbackDirection; protected Vector3 fallbackPosition; protected virtual void FindEscapeRoute() { if (currentTarget == null) return; npcPosition = transform.position; targetPosition = currentTarget.transform.position; // Direction from target to NPC directionAwayFromTarget = (npcPosition - targetPosition).normalized; // Add some random angular offset (in degrees) to create variation randomAngle = Random.Range(-45f, 45f); escapeDirection = Quaternion.Euler(0, randomAngle, 0) * directionAwayFromTarget; // Desired position to move to desiredPosition = npcPosition + escapeDirection * projectileRange; // Check for NavMesh validity if (NavMesh.SamplePosition(desiredPosition, out hitEscapeRoute, 2f, NavMesh.AllAreas)) { SetupAgentStats(hitEscapeRoute.position, true); SetAgentMoving(true); } else { // Fallback: try a few more directions for (int i = 0; i < 5; i++) { fallbackAngle = Random.Range(0f, 360f); fallbackDirection = Quaternion.Euler(0, fallbackAngle, 0) * Vector3.forward; fallbackPosition = npcPosition + fallbackDirection * (projectileRange * 0.75f); if (NavMesh.SamplePosition(fallbackPosition, out hitEscapeRoute, 2f, NavMesh.AllAreas)) { SetupAgentStats(hitEscapeRoute.position, true); SetAgentMoving(true); break; } } } } protected virtual void PatrollingUpdate() { if (!HasAvailableTargets()) { if (currentTarget != null) { currentTarget = null; } if (agent.destination == null) { PatrolNewPosition(); } else if (agent.remainingDistance < distanceToChangePatrolDestination) { PatrolNewPosition(); } else { UpdatePatrolTarget(patrolDestination); SetAgentMoving(true); } } else { //Debug.Log("Patrolling update, available targets, waiting for sight to do its job"); if (currentTarget == null) { UpdateCurrentTarget(GetClosestTarget()); } } } public virtual void OnAttackAnimationEventTriggered() { //execute ability } public virtual void OnDeathAnimationEventTriggered() { DestroyAfterEffect(); } protected virtual void OnDeath() { RPC_OnDeath(); } protected virtual void RPC_OnDeath(bool lootDropped = false) { if (isDead) return; //Debug.Log($"{this.gameObject.name} died!"); isDead = true; agent.enabled = false; animatorController.SetDead(); } protected virtual void DestroyAfterEffect() { Destroy(this.gameObject); } protected virtual void PatrolNewPosition() { agent.speed = patrolAgentSpeed; patrolDestination.x = Random.Range(-5, 5); patrolDestination.y = 0f; patrolDestination.z = Random.Range(-5, 5); UpdatePatrolTarget(transform.position + patrolDestination); SetAgentMoving(true); } protected virtual void ForceFaceTarget() { if (currentTarget == null) return; agent.transform.LookAt(currentTarget.transform.position, Vector3.up); } }