// Assets/_Project/Scripts/Gameplay/EnemyStatus.cs using System.Collections.Generic; using Unity.Netcode; using UnityEngine; using TD.Core; namespace TD.Gameplay { /// /// A single active lingering effect on an enemy. /// /// /// Magnitude semantics by source: /// /// Cold — fraction of speed retained (0.5 = half speed) /// Fire — damage per second applied as a DoT tick /// Poison — damage per second applied as a DoT tick /// Others — unused (magnitude = 0) /// /// SourceOwner carries the of the tower that /// applied this effect so DoT kill credit goes to the right player. /// public struct StatusEffect { public DamageType Source; public float Magnitude; public float RemainingDuration; public PlayerSlot SourceOwner; } /// /// Tracks and ticks lingering status effects (slow, burn, poison) on an enemy. /// /// /// Authority: The active-effect list is server-local. Only the derived /// NetworkVariable is replicated so /// EnemyMovement can scale speed on all peers. /// /// Stacking rule: Re-hitting with the same /// refreshes duration and magnitude; it does not stack. Cross-type interactions /// are not implemented; is the hook for future work. /// /// DoT damage calls with the original /// so kill attribution stays correct. /// [RequireComponent(typeof(NetworkObject))] public class EnemyStatus : NetworkBehaviour { private readonly NetworkVariable speedMultiplier = new NetworkVariable( 1f, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly List activeEffects = new List(); private EnemyHealth health; // ----- NGO lifecycle ----------------------------------------------- public override void OnNetworkSpawn() { health = GetComponent(); } // ----- Public API -------------------------------------------------- /// Current speed fraction (0–1). 1 = full speed, 0.5 = half speed. public float GetSpeedMultiplier() => speedMultiplier.Value; /// True if an effect of the given type is currently active. public bool HasEffect(DamageType type) { for (int i = 0; i < activeEffects.Count; i++) if (activeEffects[i].Source == type) return true; return false; } /// /// Applies or refreshes a lingering effect. Server-only; no-op on clients. /// Re-hitting with the same source type refreshes duration and magnitude. /// is the tower's — carried /// on the effect so DoT ticks credit the right player on a kill. /// public void ApplyEffect(DamageType source, float magnitude, float duration, PlayerSlot owner) { if (!IsServer) return; for (int i = 0; i < activeEffects.Count; i++) { if (activeEffects[i].Source != source) continue; var e = activeEffects[i]; e.Magnitude = magnitude; e.RemainingDuration = duration; e.SourceOwner = owner; activeEffects[i] = e; RecalculateSpeedMultiplier(); return; } activeEffects.Add(new StatusEffect { Source = source, Magnitude = magnitude, RemainingDuration = duration, SourceOwner = owner, }); RecalculateSpeedMultiplier(); } // ----- Server tick ------------------------------------------------ private void Update() { if (!IsServer || activeEffects.Count == 0) return; TickEffects(Time.deltaTime); } private void TickEffects(float dt) { bool anyExpired = false; for (int i = activeEffects.Count - 1; i >= 0; i--) { var e = activeEffects[i]; // DoT tick — pass the original source owner so kill credit is correct. if ((e.Source == DamageType.Fire || e.Source == DamageType.Poison) && health != null && !health.IsDead) { health.TakeDamage(e.Magnitude * dt, e.Source, e.SourceOwner); } e.RemainingDuration -= dt; if (e.RemainingDuration <= 0f) { activeEffects.RemoveAt(i); anyExpired = true; } else { activeEffects[i] = e; } } if (anyExpired) RecalculateSpeedMultiplier(); } private void RecalculateSpeedMultiplier() { float mult = 1f; for (int i = 0; i < activeEffects.Count; i++) { if (activeEffects[i].Source == DamageType.Cold) mult = Mathf.Min(mult, activeEffects[i].Magnitude); } speedMultiplier.Value = mult; } } }