using Synty.AnimationBaseLocomotion.Samples; using UnityEngine; using UnityEngine.AI; [RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(EnemyRagdoll))] [RequireComponent(typeof(NavMeshAgent))] public class EnemyAI : MonoBehaviour { public enum EnemyType { Horde, Siege } [Header("Targeting Priorities")] public EnemyType type = EnemyType.Horde; [Range(0f, 1f)] public float hordeBarrierFocusChance = 0.2f; [Header("Stats")] public int health = 30; private bool isStunned = false; [Header("Attacking")] public float attackRange = 2.5f; public float attackCooldown = 2.0f; public int attackDamage = 5; private float lastAttackTime; [Header("Blocking")] [Range(0f, 1f)] public float blockChance = 0.15f; // 15% chance to block by default [HideInInspector] public Transform currentTarget; private Animator anim; private Rigidbody rb; private EnemyRagdoll ragdoll; private NavMeshAgent agent; private Fortification targetFortification; private bool isDead = false; public GameObject stunVFX; private GameObject activeStunVFX; void Start() { anim = GetComponent(); rb = GetComponent(); ragdoll = GetComponent(); agent = GetComponent(); rb.isKinematic = true; rb.useGravity = false; EnemySwarmManager.Instance.RegisterEnemy(this); //anim.SetFloat("MoveSpeed", agent.speed); // Add a random offset to the initial attack timer so the mob doesn't attack in perfectly unified sync lastAttackTime = Time.time + Random.Range(0f, attackCooldown); //AssignTarget(); } void Update() { if (isDead || isStunned) return; // Retargeting check if a barrier is broken if (targetFortification != null && targetFortification.IsDestroyed) { targetFortification = null; AssignTarget(); } if (currentTarget != null) { Vector3 offset = currentTarget.position - transform.position; float effectiveRange = (targetFortification != null) ? attackRange * 1.5f : attackRange; // --- THE FIX: Pure mathematical distance check --- if (offset.sqrMagnitude <= effectiveRange * effectiveRange) { agent.isStopped = true; anim.SetFloat("MoveSpeed", 0f); // Face the target manually Vector3 direction = offset.normalized; direction.y = 0; // Prevent Unity error if direction is perfectly zero if (direction.sqrMagnitude > 0.001f) { transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 5f); } if (Time.time - lastAttackTime > attackCooldown) { PerformAttack(); } } else { agent.isStopped = false; agent.SetDestination(currentTarget.position); // Fallback: If agent gets stuck briefly, keep the run animation playing smoothly float currentSpeed = agent.velocity.magnitude; if (currentSpeed < 0.1f && !agent.pathPending) currentSpeed = agent.speed; anim.SetFloat("MoveSpeed", currentSpeed); } } } // --- TRIGGERED VIA ANIMATION EVENT ON THE ENEMY ATTACK CLIP --- public void ExecuteEnemyHit() { if (isDead || currentTarget == null) return; Vector3 offset = currentTarget.position - transform.position; float effectiveRange = (targetFortification != null) ? attackRange * 1.5f : attackRange; if (offset.sqrMagnitude <= effectiveRange * effectiveRange) { if (targetFortification != null && !targetFortification.IsDestroyed) { targetFortification.TakeDamage(attackDamage); } else { // Safely grab the player health without relying on the old PlayerController script PlayerHealth pHealth = currentTarget.GetComponent(); if (pHealth != null) { pHealth.TakeDamage(attackDamage); Debug.Log("Enemy landed a hit on the Player!"); } } } } private void AssignTarget() { bool prioritizeBarrier = (type == EnemyType.Siege) || (Random.value < hordeBarrierFocusChance); if (prioritizeBarrier) { targetFortification = FindNearestFortification(); if (targetFortification != null) { currentTarget = targetFortification.transform; } else { FindPlayerTarget(); } } else { FindPlayerTarget(); } } private void FindPlayerTarget() { GameObject player = GameObject.FindGameObjectWithTag("Player"); if (player != null) { currentTarget = player.transform; } else { Debug.LogWarning("EnemyAI could not find the Player! Ensure your Player Prefab has the Tag set to 'Player'."); } } private Fortification FindNearestFortification() { Fortification nearest = null; float minDist = float.MaxValue; foreach (Fortification fort in Fortification.AllFortifications) { if (fort.IsDestroyed) continue; // Ignore rubble float dist = (transform.position - fort.transform.position).sqrMagnitude; if (dist < minDist) { minDist = dist; nearest = fort; } } return nearest; } private void PerformAttack() { lastAttackTime = Time.time; // Tells the animator to play the attack animation anim.SetTrigger("Attack"); } public void TakeDamage(int damage, Vector3 hitSource, float knockbackForce) { if (isDead) return; // --- BLOCKING LOGIC --- // Check if the attack is coming from in front of the enemy Vector3 directionToHitSource = (hitSource - transform.position).normalized; float facingDotProduct = Vector3.Dot(transform.forward, directionToHitSource); // A dot product > 0.3 means the hit is roughly within a 140-degree cone in front of the enemy if (facingDotProduct > 0.3f) { if (Random.value < blockChance) { anim.SetTrigger("Block"); // Optional visual flair for blocking // Instantiate(sparksPrefab, transform.position + Vector3.up, Quaternion.identity); return; // Exit early so no damage is taken and no ragdoll occurs } } // --- DAMAGE LOGIC --- health -= damage; if (health <= 0) { Die(hitSource, knockbackForce); } else { anim.SetTrigger("Hit"); } } void Die(Vector3 hitSource, float knockbackForce) { isDead = true; EnemySwarmManager.Instance.RemoveEnemy(this); ragdoll.ActivateRagdoll(hitSource, knockbackForce); Destroy(stunVFX); Destroy(gameObject, 3f); this.enabled = false; } public void ApplyStun(float duration) { if (isDead || isStunned) return; StartCoroutine(StunRoutine(duration)); } private System.Collections.IEnumerator StunRoutine(float duration) { isStunned = true; anim.SetTrigger("Stun"); // Requires a "Stun" trigger in your Animator if (stunVFX != null && activeStunVFX == null) { activeStunVFX = Instantiate(stunVFX, transform.position, Quaternion.identity, transform); } // Detach from the Burst Compiler so they stop sliding toward you EnemySwarmManager.Instance.PauseEnemyMovement(transform); yield return new WaitForSeconds(duration); if (!isDead) { isStunned = false; anim.SetTrigger("Recover"); // Requires a "Recover" trigger in your Animator Destroy(activeStunVFX); // Reattach to the Burst Compiler so they resume the chase EnemySwarmManager.Instance.ResumeEnemyMovement(transform); } } }