using System.Collections.Generic; 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 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; private SteamCoopNetworkManager networkManager; private Transform currentTarget; private Health currentTargetHealth; private Vector3 lastTargetPosition; private float retargetTimer; private Vector3 spawnPosition; private bool cachedNavAgentState; private void Awake() { spawnPosition = transform.position; if (navMeshAgent == null) { navMeshAgent = GetComponent(); } if (weaponController == null) { weaponController = GetComponent(); } if (health == null) { health = GetComponent(); } if (identity == null) { identity = GetComponent(); } if (networkTransform == null) { networkTransform = GetComponent(); } if (navMeshAgent != null) { navMeshAgent.updateRotation = false; navMeshAgent.stoppingDistance = stoppingDistance; navMeshAgent.speed = moveSpeed; cachedNavAgentState = navMeshAgent.enabled; } lastTargetPosition = Vector3.positiveInfinity; retargetTimer = Random.Range(0f, Mathf.Max(0.05f, retargetInterval)); } private void OnEnable() { networkManager = SteamCoopNetworkManager.Instance; SyncNavMeshAgentState(ShouldSimulate()); } private void Update() { networkManager ??= SteamCoopNetworkManager.Instance; bool simulate = ShouldSimulate(); SyncNavMeshAgentState(simulate); if (weaponController != null) { weaponController.enabled = simulate; } if (!simulate) { return; } if (health != null && !health.IsAlive) { StopAgentMovement(); return; } retargetTimer -= Time.deltaTime; if (!IsTargetValid() || retargetTimer <= 0f) { AcquireTarget(); } if (currentTarget == null) { 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 void SyncNavMeshAgentState(bool simulate) { if (navMeshAgent == null) { return; } if (cachedNavAgentState == simulate) { return; } navMeshAgent.enabled = simulate; cachedNavAgentState = simulate; } private bool IsTargetValid() { if (currentTarget == null) { return false; } if (currentTargetHealth != null && !currentTargetHealth.IsAlive) { return false; } float sqrDistance = (currentTarget.position - transform.position).sqrMagnitude; if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius) { return false; } if (leashDistance > 0f) { float sqrLeash = (currentTarget.position - spawnPosition).sqrMagnitude; if (sqrLeash > leashDistance * leashDistance) { return false; } } return true; } private void AcquireTarget() { retargetTimer = retargetInterval; currentTarget = null; currentTargetHealth = null; 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; float sqrDistance = (candidateTransform.position - transform.position).sqrMagnitude; if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius) { continue; } if (sqrDistance < bestScore) { bestScore = sqrDistance; currentTarget = candidateTransform; currentTargetHealth = candidate; } } if (currentTarget == null) { lastTargetPosition = Vector3.positiveInfinity; } SharedHealthBuffer.Clear(); } private void TickMovement(float deltaTime) { Vector3 targetPosition = currentTarget.position; if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh) { navMeshAgent.speed = moveSpeed; navMeshAgent.stoppingDistance = stoppingDistance; if (!navMeshAgent.hasPath || (targetPosition - lastTargetPosition).sqrMagnitude >= repathThreshold * repathThreshold) { navMeshAgent.SetDestination(targetPosition); lastTargetPosition = targetPosition; } } else { ManualMove(targetPosition, deltaTime); } } private void ManualMove(Vector3 targetPosition, float deltaTime) { Vector3 currentPosition = transform.position; Vector3 direction = targetPosition - currentPosition; direction.y = 0f; float distance = direction.magnitude; if (distance <= Mathf.Max(0.1f, stoppingDistance)) { return; } Vector3 normalized = direction / distance; float step = moveSpeed * deltaTime; Vector3 newPosition = currentPosition + normalized * Mathf.Min(step, distance - stoppingDistance); transform.position = newPosition; } private void FaceTarget(float deltaTime) { Vector3 toTarget = currentTarget.position - 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; } if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh) { navMeshAgent.SetDestination(spawnPosition); } else { ManualMove(spawnPosition, Time.deltaTime); FacePoint(spawnPosition, Time.deltaTime); } } 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 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); detectionRadius = Mathf.Max(0f, detectionRadius); leashDistance = Mathf.Max(0f, leashDistance); retargetInterval = Mathf.Max(0.05f, retargetInterval); repathThreshold = Mathf.Max(0.1f, repathThreshold); } } }