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

155 lines
6.2 KiB
C#

// Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
using Unity.Netcode;
using UnityEngine;
using TD.Core;
namespace TD.Gameplay
{
/// <summary>
/// Per-enemy HP component. Single point through which all damage flows so
/// resistance lookups (Phase 1.5+) and kill attribution remain in one place.
/// </summary>
/// <remarks>
/// <b>Initialization:</b> Call <see cref="InitializeServer"/> on the server
/// immediately after <c>Instantiate</c> and before <c>NetworkObject.Spawn()</c>,
/// following the same pattern as <c>TowerInstance.InitializeServer</c>.
///
/// <b>Kill attribution:</b> <see cref="LastHitOwner"/> tracks the
/// <see cref="PlayerSlot"/> of the tower that most recently dealt direct damage.
/// DoT ticks from <c>EnemyStatus</c> also carry an owner so the credit
/// follows the source tower, not the DoT applicator.
///
/// <b>Death flow (server-only):</b> <see cref="TakeDamage"/> clamps HP to 0,
/// fires <see cref="OnDied"/>, then calls <c>NetworkObject.Despawn</c>.
/// Subscribers must not touch the NetworkObject after <see cref="OnDied"/> returns.
/// </remarks>
[RequireComponent(typeof(NetworkObject))]
public class EnemyHealth : NetworkBehaviour
{
// ----- Pre-spawn init (server-local) ----------------------------------
private float pendingMaxHp = 100f;
private int pendingGoldReward;
private int pendingLivesCost = 1;
private bool pendingIsFlying;
private bool hasPendingInit;
// ----- Server-local runtime state -------------------------------------
/// <summary>Gold awarded to the killing player when this enemy dies.</summary>
public int GoldReward { get; private set; }
/// <summary>Lives deducted from the shared pool when this enemy reaches the goal.</summary>
public int LivesCost { get; private set; } = 1;
/// <summary>
/// The <see cref="PlayerSlot"/> of the tower that last dealt direct damage.
/// Used by <c>WaveManager</c> to award kill gold to the correct player.
/// Updated on every <see cref="TakeDamage"/> call, including DoT ticks whose
/// source owner is tracked on <see cref="EnemyStatus.StatusEffect"/>.
/// </summary>
public PlayerSlot LastHitOwner { get; private set; } = PlayerSlot.None;
// ----- Networked state ------------------------------------------------
private readonly NetworkVariable<float> hp = new NetworkVariable<float>(
0f,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<bool> isFlying = new NetworkVariable<bool>(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
// ----- Public state ---------------------------------------------------
public float CurrentHp => hp.Value;
public float MaxHp { get; private set; } = 100f;
public bool IsDead => hp.Value <= 0f;
/// <summary>
/// True if this enemy flies over tower footprints.
/// Replicated so client visuals can adjust altitude.
/// Grounded towers with GroundedOnly=true will not target flying enemies.
/// </summary>
public bool IsFlying => isFlying.Value;
// ----- Events ---------------------------------------------------------
/// <summary>
/// Fired on the server immediately before the enemy NetworkObject is despawned.
/// <c>WaveManager</c> subscribes to credit kill gold and decrement wave count.
/// Do not access the NetworkObject after this event returns.
/// </summary>
public event System.Action<EnemyHealth> OnDied;
// ----- Server-only pre-spawn init -------------------------------------
/// <summary>
/// Called by <c>WaveManager</c> on the server after <c>Instantiate</c>
/// and before <c>NetworkObject.Spawn()</c>. Mirrors the
/// <c>TowerInstance.InitializeServer</c> pattern.
/// </summary>
public void InitializeServer(float maxHp, int goldReward, int livesCost, bool flying)
{
pendingMaxHp = maxHp;
pendingGoldReward = goldReward;
pendingLivesCost = livesCost;
pendingIsFlying = flying;
hasPendingInit = true;
// Cache locally on the server immediately — clients resolve via NV.
MaxHp = maxHp;
GoldReward = goldReward;
LivesCost = livesCost;
}
// ----- NGO lifecycle --------------------------------------------------
public override void OnNetworkSpawn()
{
if (IsServer && hasPendingInit)
{
hp.Value = pendingMaxHp;
isFlying.Value = pendingIsFlying;
hasPendingInit = false;
}
// Non-server clients resolve MaxHp from the replicated hp initial value.
if (!IsServer)
MaxHp = hp.Value;
}
// ----- Server API -----------------------------------------------------
/// <summary>
/// Applies damage to this enemy. Server-only; silently no-ops on clients.
/// <paramref name="type"/> is accepted for future resistance lookups (Phase 1.5+).
/// <paramref name="attackerSlot"/> identifies the tower owner for kill attribution.
/// </summary>
public void TakeDamage(float damage, DamageType type, PlayerSlot attackerSlot)
{
if (!IsServer) return;
if (IsDead) return;
// STUB — resistance table slot:
// float modified = ResistanceTable.Apply(damage, type, this);
float modified = damage;
LastHitOwner = attackerSlot;
hp.Value = Mathf.Max(0f, hp.Value - modified);
if (hp.Value <= 0f)
HandleDeath();
}
// ----- Private --------------------------------------------------------
private void HandleDeath()
{
OnDied?.Invoke(this);
NetworkObject.Despawn();
}
}
}