UnityTowerDefense/Assets/_Project/Scripts/Gameplay/EnemyStatus.cs

160 lines
5.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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>
/// </remarks>
public struct StatusEffect
{
public DamageType Source;
public float Magnitude;
public float RemainingDuration;
}
/// <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 (not replicated).
/// Only the derived <see cref="speedMultiplier"/> NetworkVariable is replicated,
/// so <c>EnemyMovement</c> (Phase 1.5/1.6) can scale speed on all peers without
/// re-broadcasting the full effect list.
///
/// <b>Stacking rule:</b> A second hit of the same <see cref="DamageType"/> refreshes
/// the duration and magnitude rather than stacking. Cross-type interactions (e.g.
/// Cold + Fire) are not yet implemented; <see cref="HasEffect"/> is the hook for
/// when that design is worked out.
///
/// <b>DoT damage</b> is applied by calling <see cref="EnemyHealth.TakeDamage"/> each
/// tick so resistance lookups remain in one place.
/// </remarks>
[RequireComponent(typeof(NetworkObject))]
public class EnemyStatus : NetworkBehaviour
{
// Replicated so EnemyMovement can read it on all clients without
// knowing anything about which effects are active.
private readonly NetworkVariable<float> speedMultiplier = new NetworkVariable<float>(
1f,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
// Server-local — only the derived speedMultiplier NV crosses the wire.
private readonly List<StatusEffect> activeEffects = new List<StatusEffect>();
// Resolved once; used by Tick for DoT TakeDamage calls.
private EnemyHealth health;
// ----- NGO lifecycle -----------------------------------------------
public override void OnNetworkSpawn()
{
health = GetComponent<EnemyHealth>();
}
// ----- Public API --------------------------------------------------
/// <summary>Current speed fraction (01). 1 = full speed, 0.5 = half speed, etc.</summary>
public float GetSpeedMultiplier() => speedMultiplier.Value;
/// <summary>True if an effect of the given type is currently active on this enemy.</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 damage type refreshes duration and magnitude.
/// </summary>
public void ApplyEffect(DamageType source, float magnitude, float duration)
{
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;
activeEffects[i] = e;
RecalculateSpeedMultiplier();
return;
}
activeEffects.Add(new StatusEffect
{
Source = source,
Magnitude = magnitude,
RemainingDuration = duration,
});
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];
// Apply DoT for Fire and Poison.
if (e.Source == DamageType.Fire || e.Source == DamageType.Poison)
{
if (health != null && !health.IsDead)
health.TakeDamage(e.Magnitude * dt, e.Source);
}
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;
}
}
}