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

162 lines
5.8 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>
/// <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 (01). 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;
}
}
}