Files
beyond/Assets/AI/_Summons/SwarmAgent.cs
SzymonMis 1beff44ada Summons
2026-01-20 21:04:22 +01:00

379 lines
9.6 KiB
C#

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<NavMeshAgent>();
animator = GetComponentInChildren<Animator>();
meleeAttackObjects = GetComponentsInChildren<vMeleeAttackObject>(true);
if (coordinator == null)
coordinator = GetComponentInParent<SwarmCoordinator>();
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<SwarmAgent> 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;
}
}