273 lines
8.6 KiB
C#
273 lines
8.6 KiB
C#
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<Animator>();
|
|
rb = GetComponent<Rigidbody>();
|
|
ragdoll = GetComponent<EnemyRagdoll>();
|
|
agent = GetComponent<NavMeshAgent>();
|
|
|
|
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<PlayerHealth>();
|
|
if (pHealth != null)
|
|
{
|
|
pHealth.TakeDamage(attackDamage);
|
|
Debug.Log("<color=red>Enemy landed a hit on the Player!</color>");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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("<color=red>EnemyAI could not find the Player! Ensure your Player Prefab has the Tag set to 'Player'.</color>");
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
} |