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); private static int nextEnemyNetworkId = StartingEnemyNetworkId; private const int StartingEnemyNetworkId = 10000; [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 currentTargetTransform; private NetworkIdentity currentTargetIdentity; private Health currentTargetHealth; private Vector3 lastTargetPosition; private Vector3 lastKnownTargetPosition; private float retargetTimer; private Vector3 spawnPosition; private bool cachedNavAgentState; private void Awake() { EnsureIdentity(); spawnPosition = transform.position; if (navMeshAgent == null) { navMeshAgent = GetComponent(); } if (weaponController == null) { weaponController = GetComponent(); } if (health == null) { health = GetComponent(); } if (networkTransform == null) { networkTransform = GetComponent(); } if (navMeshAgent != null) { navMeshAgent.updateRotation = false; navMeshAgent.stoppingDistance = stoppingDistance; navMeshAgent.speed = moveSpeed; cachedNavAgentState = navMeshAgent.enabled; } lastTargetPosition = Vector3.positiveInfinity; lastKnownTargetPosition = spawnPosition; retargetTimer = Random.Range(0f, Mathf.Max(0.05f, retargetInterval)); } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void ResetNetworkIdCounter() { nextEnemyNetworkId = StartingEnemyNetworkId; } private void EnsureIdentity() { if (identity == null) { identity = GetComponent(); } if (identity == null) { return; } if (identity.NetworkId == 0) { identity.SetNetworkId(nextEnemyNetworkId++); } } 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 (!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 void SyncNavMeshAgentState(bool simulate) { if (navMeshAgent == null) { 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; } 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); lastTargetPosition = targetPosition; } } 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) { 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; } if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh) { navMeshAgent.SetDestination(spawnPosition); } else { ManualMove(spawnPosition, Time.deltaTime); 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 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); } } }