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