using Synty.AnimationBaseLocomotion.Samples; using UnityEngine; [RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(EnemyRagdoll))] public class EnemyAI : MonoBehaviour { [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 private Animator anim; private Rigidbody rb; private EnemyRagdoll ragdoll; private bool isDead = false; void Start() { anim = GetComponent(); rb = GetComponent(); ragdoll = GetComponent(); rb.isKinematic = true; rb.useGravity = false; EnemySwarmManager.Instance.RegisterEnemy(transform); anim.SetFloat("MoveSpeed", 3.5f); // 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); } void Update() { if (isDead || isStunned) return; // PlayerController.Instance comes from the core script setup if (SamplePlayerAnimationController.Instance != null) { Vector3 offset = SamplePlayerAnimationController.Instance.transform.position - transform.position; // sqrMagnitude is significantly faster for the CPU than Vector3.Distance if (offset.sqrMagnitude <= attackRange * attackRange) { if (Time.time - lastAttackTime > attackCooldown) { PerformAttack(); } } } } private void PerformAttack() { lastAttackTime = Time.time; // Tells the animator to play the attack animation anim.SetTrigger("Attack"); } // --- TRIGGERED VIA ANIMATION EVENT ON THE ENEMY ATTACK CLIP --- public void ExecuteEnemyHit() { if (isDead || SamplePlayerAnimationController.Instance == null) return; // Simple check to see if the player is still in range when the attack lands Vector3 offset = SamplePlayerAnimationController.Instance.transform.position - transform.position; if (offset.sqrMagnitude <= attackRange * attackRange) { SamplePlayerAnimationController.Instance.GetComponent().TakeDamage(attackDamage); } } 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(transform); ragdoll.ActivateRagdoll(hitSource, knockbackForce); 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 // 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 // Reattach to the Burst Compiler so they resume the chase EnemySwarmManager.Instance.ResumeEnemyMovement(transform); } } }