// 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;
}
}
}