using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Invector.vMelee; public class SwarmAgent : MonoBehaviour { public enum SwarmProfile { Default, Spider, Wolf, Dragonfly } private enum SwarmState { Idle, Chase, Attack, Retreat } [Header("Profile")] [SerializeField] private bool autoProfileByName = true; [SerializeField] private SwarmProfile profileOverride = SwarmProfile.Default; [Header("Targeting")] [SerializeField] private SwarmCoordinator coordinator; [SerializeField] private string playerTag = "Player"; [SerializeField] private float targetRefreshInterval = 1.0f; [Header("Movement")] [SerializeField] private float moveSpeed = 3.5f; [SerializeField] private float acceleration = 8f; [SerializeField] private float angularSpeed = 360f; [SerializeField] private float stoppingDistance = 1.6f; [SerializeField] private float repathInterval = 0.25f; [Header("Attack")] [SerializeField] private float attackRange = 1.8f; [SerializeField] private Vector2Int attacksPerBurst = new Vector2Int(1, 2); [SerializeField] private float attackCooldown = 0.8f; [SerializeField] private float damageEnableDelay = 0.1f; [SerializeField] private float damageWindow = 0.25f; [SerializeField] private string[] attackTriggers = new string[] { "Attack" }; [Header("Retreat")] [SerializeField] private float retreatDuration = 1.25f; [SerializeField] private float retreatDistance = 3.5f; [Header("Spider Strafe")] [SerializeField] private float strafeRadius = 2.5f; [SerializeField] private float strafeSpeed = 2.0f; [Header("Wolf Flank")] [SerializeField] private float flankDistance = 2.5f; [SerializeField] private float flankSideOffset = 2.0f; [SerializeField] private float flankReplanInterval = 0.6f; [Header("Dragonfly Figure 8")] [SerializeField] private float figureEightRadius = 3.0f; [SerializeField] private float figureEightSpeed = 2.2f; private SwarmProfile profile; private SwarmState state = SwarmState.Idle; private NavMeshAgent agent; private Animator animator; private vMeleeAttackObject[] meleeAttackObjects; private Transform target; private float nextTargetRefreshTime; private float nextRepathTime; private float nextTickTime; private float lastAttackTime; private float retreatEndTime; private float flankSide = 1f; private float nextFlankReplanTime; private Vector3 retreatCenter; private float retreatStartTime; private Coroutine attackRoutine; private void Awake() { agent = GetComponent(); animator = GetComponentInChildren(); meleeAttackObjects = GetComponentsInChildren(true); if (coordinator == null) coordinator = GetComponentInParent(); if (autoProfileByName) profile = ResolveProfileByName(gameObject.name); else profile = profileOverride; } private void OnEnable() { if (coordinator != null) coordinator.Register(this); DisableDamage(); ConfigureAgent(); } private void OnDisable() { if (coordinator != null) coordinator.Unregister(this); } private void Update() { if (Time.time < nextTickTime) return; nextTickTime = Time.time + GetUpdateInterval(); Tick(); } private void Tick() { RefreshTargetIfNeeded(); if (target == null) { state = SwarmState.Idle; if (agent != null) agent.isStopped = true; return; } if (state == SwarmState.Retreat) { UpdateRetreat(); return; } if (state == SwarmState.Attack) return; float sqrDist = (target.position - transform.position).sqrMagnitude; float attackRangeSqr = attackRange * attackRange; if (sqrDist <= attackRangeSqr && Time.time >= lastAttackTime + attackCooldown) { StartAttackBurst(); return; } UpdateChase(); } private void UpdateChase() { if (agent == null) return; state = SwarmState.Chase; agent.isStopped = false; if (Time.time < nextRepathTime) return; nextRepathTime = Time.time + repathInterval; Vector3 desired = GetDesiredPosition(); agent.SetDestination(desired); } private void StartAttackBurst() { if (attackRoutine != null) StopCoroutine(attackRoutine); attackRoutine = StartCoroutine(AttackBurstCoroutine()); } private IEnumerator AttackBurstCoroutine() { state = SwarmState.Attack; lastAttackTime = Time.time; if (agent != null) agent.isStopped = true; int minAttacks = Mathf.Max(1, attacksPerBurst.x); int maxAttacks = Mathf.Max(minAttacks, attacksPerBurst.y); int count = Random.Range(minAttacks, maxAttacks + 1); for (int i = 0; i < count; i++) { TriggerAttackAnimation(); yield return StartCoroutine(DamageWindowCoroutine()); yield return new WaitForSeconds(attackCooldown); } StartRetreat(); } private IEnumerator DamageWindowCoroutine() { if (damageEnableDelay > 0f) yield return new WaitForSeconds(damageEnableDelay); EnableDamage(); if (damageWindow > 0f) yield return new WaitForSeconds(damageWindow); DisableDamage(); } private void StartRetreat() { state = SwarmState.Retreat; retreatStartTime = Time.time; retreatEndTime = retreatStartTime + retreatDuration; if (target != null) retreatCenter = target.position; else retreatCenter = transform.position; if (agent != null) agent.isStopped = false; } private void UpdateRetreat() { if (agent == null) return; if (Time.time >= retreatEndTime) { state = SwarmState.Chase; return; } if (Time.time < nextRepathTime) return; nextRepathTime = Time.time + repathInterval; Vector3 retreatTarget; if (profile == SwarmProfile.Dragonfly) { float t = (Time.time - retreatStartTime) * figureEightSpeed; float x = Mathf.Sin(t); float z = Mathf.Sin(t * 2f) * 0.5f; Vector3 offset = new Vector3(x, 0f, z) * figureEightRadius; retreatTarget = retreatCenter + offset; } else { Vector3 away = (transform.position - target.position).normalized; retreatTarget = transform.position + away * retreatDistance; } agent.SetDestination(retreatTarget); } private Vector3 GetDesiredPosition() { Vector3 baseTarget = target.position; Vector3 separation = GetSeparationOffset(); switch (profile) { case SwarmProfile.Spider: return baseTarget + GetStrafeOffset() + separation; case SwarmProfile.Wolf: return GetFlankPosition() + separation; case SwarmProfile.Dragonfly: return baseTarget + separation; default: return baseTarget + separation; } } private Vector3 GetSeparationOffset() { if (coordinator == null) return Vector3.zero; float radius = coordinator.SeparationRadius; if (radius <= 0f) return Vector3.zero; List agents = coordinator.GetAgents(); if (agents == null || agents.Count == 0) return Vector3.zero; Vector3 offset = Vector3.zero; float radiusSqr = radius * radius; Vector3 pos = transform.position; for (int i = 0; i < agents.Count; i++) { SwarmAgent other = agents[i]; if (other == null || other == this) continue; Vector3 delta = pos - other.transform.position; float distSqr = delta.sqrMagnitude; if (distSqr > 0f && distSqr < radiusSqr) offset += delta.normalized * (1f - (distSqr / radiusSqr)); } return offset * coordinator.SeparationWeight; } private Vector3 GetStrafeOffset() { Vector3 toTarget = (target.position - transform.position).normalized; Vector3 right = new Vector3(-toTarget.z, 0f, toTarget.x); float wave = Mathf.Sin(Time.time * strafeSpeed); return right * (wave * strafeRadius); } private Vector3 GetFlankPosition() { if (Time.time >= nextFlankReplanTime) { nextFlankReplanTime = Time.time + flankReplanInterval; flankSide = Random.value < 0.5f ? -1f : 1f; } Vector3 behind = -target.forward * flankDistance; Vector3 side = target.right * (flankSide * flankSideOffset); Vector3 desired = target.position + behind + side; return desired; } private void ConfigureAgent() { if (agent == null) return; agent.speed = moveSpeed; agent.acceleration = acceleration; agent.angularSpeed = angularSpeed; agent.stoppingDistance = stoppingDistance; } private void RefreshTargetIfNeeded() { if (coordinator != null && coordinator.Target != null) { target = coordinator.Target; return; } if (Time.time < nextTargetRefreshTime) return; nextTargetRefreshTime = Time.time + targetRefreshInterval; GameObject player = GameObject.FindGameObjectWithTag(playerTag); if (player != null) target = player.transform; } private void TriggerAttackAnimation() { if (animator == null || attackTriggers == null || attackTriggers.Length == 0) return; string trigger = attackTriggers[Random.Range(0, attackTriggers.Length)]; if (!string.IsNullOrEmpty(trigger)) animator.SetTrigger(trigger); } private void EnableDamage() { for (int i = 0; i < meleeAttackObjects.Length; i++) { if (meleeAttackObjects[i] != null) meleeAttackObjects[i].SetActiveDamage(true); } } private void DisableDamage() { for (int i = 0; i < meleeAttackObjects.Length; i++) { if (meleeAttackObjects[i] != null) meleeAttackObjects[i].SetActiveDamage(false); } } private float GetUpdateInterval() { if (coordinator == null) return 0.2f; float jitter = coordinator.UpdateJitter; return coordinator.UpdateInterval + (jitter > 0f ? Random.Range(-jitter, jitter) : 0f); } private SwarmProfile ResolveProfileByName(string objectName) { string nameLower = objectName.ToLowerInvariant(); if (nameLower.Contains("spider")) return SwarmProfile.Spider; if (nameLower.Contains("dog") || nameLower.Contains("barghest") || nameLower.Contains("golem")) return SwarmProfile.Wolf; if (nameLower.Contains("bug") || nameLower.Contains("flying")) return SwarmProfile.Dragonfly; return SwarmProfile.Default; } }