Adding major combat changes and features

This commit is contained in:
Matt F 2026-05-12 21:31:10 -07:00
parent abcefcd7f1
commit 42ee0bf65d
28 changed files with 1653 additions and 46 deletions

View file

@ -0,0 +1,160 @@
// 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;
}
}
}