using System.Collections.Generic; using Game.Scripts.Runtime.Data; using Game.Scripts.Runtime.Pooling; using Game.Scripts.Runtime.Navigation; using MegaKoop.Game.Combat; using MegaKoop.Game.Networking; using MegaKoop.Game.WeaponSystem; using UnityEngine; using UnityEngine.AI; namespace MegaKoop.Game.Enemy { /// /// Host-driven enemy brain that chases the closest hero, fires using the shared weapon system, /// and mirrors movement state to peers through SteamCoopNetworkManager. /// [DisallowMultipleComponent] [RequireComponent(typeof(NetworkIdentity))] [RequireComponent(typeof(Health))] [RequireComponent(typeof(SteamNetworkTransform))] [RequireComponent(typeof(WeaponController))] public class SteamEnemyController : MonoBehaviour { private static readonly List SharedHealthBuffer = new(32); [Header("Movement")] [SerializeField] private float moveSpeed = 3.5f; [SerializeField] private float turnSpeed = 6f; [SerializeField] private float stoppingDistance = 4f; [SerializeField] private float attackStoppingDistance = 1.25f; [SerializeField] private float detectionRadius = 30f; [SerializeField] private float leashDistance = 60f; [SerializeField] private float retargetInterval = 0.4f; [SerializeField] private float repathThreshold = 1.5f; [SerializeField] private bool returnToSpawnWhenIdle = true; [Header("References")] [SerializeField] private NavMeshAgent navMeshAgent; [SerializeField] private WeaponController weaponController; [SerializeField] private Health health; [SerializeField] private NetworkIdentity identity; [SerializeField] private SteamNetworkTransform networkTransform; [Header("Spawner Integration")] [SerializeField] private EnemyDefinition fallbackDefinition; [SerializeField] private bool applyDefinitionStats = true; [SerializeField] private float healthMultiplier = 1f; [SerializeField] private float moveSpeedMultiplier = 1f; [SerializeField] private float damageMultiplier = 1f; [SerializeField] private float deathDespawnDelay = 0f; private SteamCoopNetworkManager networkManager; private Transform currentTargetTransform; private NetworkIdentity currentTargetIdentity; private Health currentTargetHealth; private Vector3 lastTargetPosition; private Vector3 lastKnownTargetPosition; private float retargetTimer; private Vector3 spawnPosition; private bool cachedNavAgentState; private PooledInstance pooledInstance; private EnemyDefinition activeDefinition; private float baseMoveSpeed; private bool pendingDespawn; private float despawnTimer; private void Awake() { spawnPosition = transform.position; baseMoveSpeed = moveSpeed; if (navMeshAgent == null) { navMeshAgent = GetComponent(); } if (weaponController == null) { weaponController = GetComponent(); } if (health == null) { health = GetComponent(); } if (networkTransform == null) { networkTransform = GetComponent(); } if (pooledInstance == null) { pooledInstance = GetComponent(); } var navMeshAvailable = NavMeshRuntimeUtility.HasNavMeshData(); if (navMeshAgent != null) { navMeshAgent.updateRotation = false; navMeshAgent.stoppingDistance = stoppingDistance; navMeshAgent.speed = moveSpeed; if (!navMeshAvailable && navMeshAgent.enabled) { navMeshAgent.enabled = false; } cachedNavAgentState = navMeshAgent.enabled; } lastTargetPosition = Vector3.positiveInfinity; lastKnownTargetPosition = spawnPosition; retargetTimer = Random.Range(0f, Mathf.Max(0.05f, retargetInterval)); } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void ResetNetworkIdCounter() { NetworkIdAllocator.Reset(); } private void EnsureIdentity() { if (identity == null) { identity = GetComponent(); } if (identity == null) { return; } if (identity.NetworkId != 0) { return; } if (!ShouldAssignLocalNetworkId()) { return; } int allocatedId = NetworkIdAllocator.AllocateEnemyId(); identity.SetNetworkId(allocatedId); } private void OnEnable() { EnsureIdentity(); networkManager = SteamCoopNetworkManager.Instance; pooledInstance ??= GetComponent(); pendingDespawn = false; despawnTimer = 0f; spawnPosition = transform.position; lastKnownTargetPosition = spawnPosition; lastTargetPosition = Vector3.positiveInfinity; ClearTarget(); retargetTimer = Random.Range(0f, Mathf.Max(0.05f, retargetInterval)); SyncNavMeshAgentState(ShouldSimulate()); if (health != null) { health.NormalizedHealthChanged += HandleHealthChanged; } ApplyDefinitionStats(); } private void OnDisable() { if (health != null) { health.NormalizedHealthChanged -= HandleHealthChanged; } pendingDespawn = false; weaponController?.ClearDamageOverride(); } private void Update() { networkManager ??= SteamCoopNetworkManager.Instance; bool simulate = ShouldSimulate(); SyncNavMeshAgentState(simulate); if (weaponController != null) { weaponController.enabled = simulate; } if (pendingDespawn) { TickDespawn(Time.deltaTime); return; } if (!simulate) { return; } if (health != null && !health.IsAlive) { BeginDespawnCountdown(); return; } retargetTimer -= Time.deltaTime; if (!IsTargetValid() || retargetTimer <= 0f) { AcquireTarget(); } if (!TryGetTargetPosition(out _)) { HandleIdle(); return; } TickMovement(Time.deltaTime); FaceTarget(Time.deltaTime); } private bool ShouldSimulate() { if (networkManager == null) { return true; } if (!networkManager.IsConnected) { return true; } return networkManager.IsHost; } private bool ShouldAssignLocalNetworkId() { var manager = SteamCoopNetworkManager.Instance; if (manager == null) { return true; } if (!manager.IsConnected) { return true; } return manager.IsHost; } private void SyncNavMeshAgentState(bool simulate) { if (navMeshAgent == null) { return; } if (!NavMeshRuntimeUtility.HasNavMeshData()) { if (navMeshAgent.enabled) { navMeshAgent.enabled = false; } cachedNavAgentState = false; return; } if (cachedNavAgentState == simulate) { return; } navMeshAgent.enabled = simulate; cachedNavAgentState = simulate; } private bool IsTargetValid() { if (currentTargetTransform == null && currentTargetIdentity == null && currentTargetHealth == null) { return false; } if (currentTargetHealth != null && !currentTargetHealth.IsAlive) { return false; } if (!TryGetTargetPosition(out Vector3 targetPosition)) { return false; } float sqrDistance = (targetPosition - transform.position).sqrMagnitude; if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius) { return false; } if (leashDistance > 0f) { float sqrLeash = (targetPosition - spawnPosition).sqrMagnitude; if (sqrLeash > leashDistance * leashDistance) { return false; } } return true; } private void AcquireTarget() { retargetTimer = retargetInterval; ClearTarget(); float bestScore = float.MaxValue; SharedHealthBuffer.Clear(); SharedHealthBuffer.AddRange(FindObjectsOfType(false)); foreach (Health candidate in SharedHealthBuffer) { if (candidate == null || candidate == health) { continue; } if (!candidate.IsAlive || candidate.Team != Team.Heroes) { continue; } Transform candidateTransform = candidate.transform; NetworkIdentity candidateIdentity = candidate.GetComponent(); Vector3 candidatePosition; if (candidateIdentity != null && SteamCharacterStateCache.TryGetState(candidateIdentity.NetworkId, out var state)) { candidatePosition = state.Position; } else { candidatePosition = candidateTransform.position; } float sqrDistance = (candidatePosition - transform.position).sqrMagnitude; if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius) { continue; } if (sqrDistance < bestScore) { bestScore = sqrDistance; currentTargetTransform = candidateTransform; currentTargetIdentity = candidateIdentity; currentTargetHealth = candidate; lastKnownTargetPosition = candidatePosition; lastTargetPosition = Vector3.positiveInfinity; } } if (currentTargetTransform == null && currentTargetIdentity == null) { lastTargetPosition = Vector3.positiveInfinity; } SharedHealthBuffer.Clear(); } private void TickMovement(float deltaTime) { if (!TryGetTargetPosition(out Vector3 targetPosition)) { ClearTarget(); return; } float desiredStopDistance = GetDesiredStoppingDistance(); if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh) { navMeshAgent.speed = moveSpeed; navMeshAgent.stoppingDistance = desiredStopDistance; if (!navMeshAgent.hasPath || (targetPosition - lastTargetPosition).sqrMagnitude >= repathThreshold * repathThreshold) { navMeshAgent.SetDestination(targetPosition); lastTargetPosition = targetPosition; } } else { ManualMove(targetPosition, deltaTime, desiredStopDistance); lastTargetPosition = targetPosition; } } private void ManualMove(Vector3 targetPosition, float deltaTime, float stopDistance) { Vector3 currentPosition = transform.position; Vector3 direction = targetPosition - currentPosition; direction.y = 0f; float distance = direction.magnitude; float resolvedStop = Mathf.Max(0.1f, stopDistance); if (distance <= resolvedStop) { return; } Vector3 normalized = direction / distance; float step = moveSpeed * deltaTime; Vector3 newPosition = currentPosition + normalized * Mathf.Min(step, distance - resolvedStop); transform.position = newPosition; } private void FaceTarget(float deltaTime) { if (!TryGetTargetPosition(out Vector3 targetPosition)) { return; } Vector3 toTarget = targetPosition - transform.position; toTarget.y = 0f; if (toTarget.sqrMagnitude < 0.0001f) { return; } Quaternion desiredRotation = Quaternion.LookRotation(toTarget.normalized, Vector3.up); transform.rotation = Quaternion.Slerp(transform.rotation, desiredRotation, Mathf.Clamp01(turnSpeed * deltaTime)); } private void HandleIdle() { StopAgentMovement(); if (!returnToSpawnWhenIdle) { return; } if ((transform.position - spawnPosition).sqrMagnitude <= 0.25f) { return; } float stopDistance = Mathf.Max(0.1f, stoppingDistance); if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh) { navMeshAgent.SetDestination(spawnPosition); } else { ManualMove(spawnPosition, Time.deltaTime, stopDistance); FacePoint(spawnPosition, Time.deltaTime); } } private bool TryGetTargetPosition(out Vector3 position) { if (currentTargetIdentity != null && SteamCharacterStateCache.TryGetState(currentTargetIdentity.NetworkId, out var state)) { position = state.Position; lastKnownTargetPosition = position; return true; } if (currentTargetTransform != null) { position = currentTargetTransform.position; lastKnownTargetPosition = position; return true; } if (currentTargetHealth != null) { position = currentTargetHealth.transform.position; lastKnownTargetPosition = position; return true; } if (lastKnownTargetPosition != Vector3.positiveInfinity) { position = lastKnownTargetPosition; return true; } position = Vector3.zero; return false; } private void ClearTarget() { currentTargetTransform = null; currentTargetIdentity = null; currentTargetHealth = null; lastKnownTargetPosition = Vector3.positiveInfinity; } private void FacePoint(Vector3 point, float deltaTime) { Vector3 toPoint = point - transform.position; toPoint.y = 0f; if (toPoint.sqrMagnitude < 0.0001f) { return; } Quaternion desiredRotation = Quaternion.LookRotation(toPoint.normalized, Vector3.up); transform.rotation = Quaternion.Slerp(transform.rotation, desiredRotation, Mathf.Clamp01(turnSpeed * deltaTime)); } private void StopAgentMovement() { if (navMeshAgent != null && navMeshAgent.enabled) { navMeshAgent.ResetPath(); } } private void HandleHealthChanged(float normalized) { if (normalized > 0f || pendingDespawn) { return; } BeginDespawnCountdown(); } private void BeginDespawnCountdown() { if (pendingDespawn) { return; } pendingDespawn = true; despawnTimer = Mathf.Max(0f, deathDespawnDelay); StopAgentMovement(); } private void TickDespawn(float deltaTime) { if (!pendingDespawn) { return; } despawnTimer -= deltaTime; if (despawnTimer <= 0f) { CompleteDespawn(); } } private void CompleteDespawn() { pendingDespawn = false; if (pooledInstance != null) { pooledInstance.ReturnToPool(); } else { gameObject.SetActive(false); } } private void ApplyDefinitionStats() { if (health == null && weaponController == null && navMeshAgent == null && !applyDefinitionStats) { return; } activeDefinition = ResolveDefinition(); if (health != null) { float resolvedHp = health.MaxHealth; if (applyDefinitionStats && activeDefinition != null && activeDefinition.BaseHP > 0f) { resolvedHp = activeDefinition.BaseHP; } resolvedHp *= Mathf.Max(0.01f, healthMultiplier); health.Revive(resolvedHp); } float resolvedMoveSpeed = baseMoveSpeed; if (applyDefinitionStats && activeDefinition != null && activeDefinition.MoveSpeed > 0f) { resolvedMoveSpeed = activeDefinition.MoveSpeed; } resolvedMoveSpeed *= Mathf.Max(0.01f, moveSpeedMultiplier); moveSpeed = resolvedMoveSpeed; if (navMeshAgent != null) { navMeshAgent.speed = moveSpeed; } if (weaponController != null) { if (applyDefinitionStats && activeDefinition != null && activeDefinition.Damage > 0f) { float resolvedDamage = activeDefinition.Damage * Mathf.Max(0.01f, damageMultiplier); weaponController.SetDamageOverride(resolvedDamage); } else { weaponController.ClearDamageOverride(); } } } private EnemyDefinition ResolveDefinition() { if (pooledInstance != null && pooledInstance.Definition != null) { return pooledInstance.Definition; } return fallbackDefinition; } private void OnDrawGizmosSelected() { Gizmos.color = new Color(1f, 0.25f, 0f, 0.2f); Gizmos.DrawWireSphere(transform.position, detectionRadius); if (leashDistance > 0f) { Gizmos.color = new Color(0.2f, 0.4f, 1f, 0.2f); Vector3 leashCenter = Application.isPlaying ? spawnPosition : transform.position; Gizmos.DrawWireSphere(leashCenter, leashDistance); } } private void OnValidate() { moveSpeed = Mathf.Max(0f, moveSpeed); turnSpeed = Mathf.Max(0f, turnSpeed); stoppingDistance = Mathf.Max(0f, stoppingDistance); attackStoppingDistance = Mathf.Max(0f, attackStoppingDistance); detectionRadius = Mathf.Max(0f, detectionRadius); leashDistance = Mathf.Max(0f, leashDistance); retargetInterval = Mathf.Max(0.05f, retargetInterval); repathThreshold = Mathf.Max(0.1f, repathThreshold); } private float GetDesiredStoppingDistance() { if (HasAggroTarget()) { return Mathf.Max(0.05f, attackStoppingDistance); } return Mathf.Max(0.05f, stoppingDistance); } private bool HasAggroTarget() { return currentTargetTransform != null || currentTargetIdentity != null || currentTargetHealth != null; } } }