// Assets/_Project/Scripts/Gameplay/EnemyHealth.cs using Unity.Netcode; using UnityEngine; using TD.Core; namespace TD.Gameplay { /// /// Per-enemy HP component. Single point through which all damage flows so /// resistance lookups (Phase 1.5+) and kill attribution remain in one place. /// /// /// Initialization: Call on the server /// immediately after Instantiate and before NetworkObject.Spawn(), /// following the same pattern as TowerInstance.InitializeServer. /// /// Kill attribution: tracks the /// of the tower that most recently dealt direct damage. /// DoT ticks from EnemyStatus also carry an owner so the credit /// follows the source tower, not the DoT applicator. /// /// Death flow (server-only): clamps HP to 0, /// fires , then calls NetworkObject.Despawn. /// Subscribers must not touch the NetworkObject after returns. /// [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 ------------------------------------- /// Gold awarded to the killing player when this enemy dies. public int GoldReward { get; private set; } /// Lives deducted from the shared pool when this enemy reaches the goal. public int LivesCost { get; private set; } = 1; /// /// The of the tower that last dealt direct damage. /// Used by WaveManager to award kill gold to the correct player. /// Updated on every call, including DoT ticks whose /// source owner is tracked on . /// public PlayerSlot LastHitOwner { get; private set; } = PlayerSlot.None; // ----- Networked state ------------------------------------------------ private readonly NetworkVariable hp = new NetworkVariable( 0f, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable isFlying = new NetworkVariable( 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; /// /// 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. /// public bool IsFlying => isFlying.Value; // ----- Events --------------------------------------------------------- /// /// Fired on the server immediately before the enemy NetworkObject is despawned. /// WaveManager subscribes to credit kill gold and decrement wave count. /// Do not access the NetworkObject after this event returns. /// public event System.Action OnDied; // ----- Server-only pre-spawn init ------------------------------------- /// /// Called by WaveManager on the server after Instantiate /// and before NetworkObject.Spawn(). Mirrors the /// TowerInstance.InitializeServer pattern. /// 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 ----------------------------------------------------- /// /// Applies damage to this enemy. Server-only; silently no-ops on clients. /// is accepted for future resistance lookups (Phase 1.5+). /// identifies the tower owner for kill attribution. /// 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(); } } }