// Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
using System.Collections;
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
/// and fires immediately so wave bookkeeping and gold
/// award happen the moment HP hits zero. The component then disables
/// and all child Colliders, triggers the
/// Die animator parameter (synced via NetworkAnimator), waits
/// seconds, sinks the transform
/// units over seconds,
/// and finally calls NetworkObject.Despawn.
///
[RequireComponent(typeof(NetworkObject))]
public class EnemyHealth : NetworkBehaviour, ISelectable
{
// ----- Inspector ------------------------------------------------------
[Header("Identity")]
[Tooltip("The EnemyDefinition this prefab represents. Drives the HUD info " +
"panel (name, speed, gold bounty) when the enemy is selected. " +
"Must be assigned on the prefab so it's available on every peer " +
"without needing a registry lookup.")]
[SerializeField] private EnemyDefinition definition;
/// The static definition this enemy was spawned from.
public EnemyDefinition Definition => definition;
[Header("Death sequence")]
[Tooltip("Animator that plays the death animation. Trigger parameter 'Die' " +
"is set on death. Leave null to skip the death animation.")]
[SerializeField] private Animator deathAnimator;
[Tooltip("Seconds to hold after the Die trigger fires before the corpse starts sinking. " +
"Should match the death animation clip's duration.")]
[SerializeField] private float deathAnimationDuration = 2f;
[Tooltip("Seconds spent sinking into the ground.")]
[SerializeField] private float sinkDuration = 1.5f;
[Tooltip("How far (world units) the corpse sinks before despawning.")]
[SerializeField] private float sinkDepth = 1.5f;
private static readonly int DieHash = Animator.StringToHash("Die");
// ----- Pre-spawn init (server-local) ----------------------------------
private float pendingMaxHp = 100f;
private int pendingLivesCost = 1;
private bool pendingIsFlying;
private bool hasPendingInit;
// ----- Server-local runtime state -------------------------------------
// Kill gold is no longer carried per-enemy — it comes from
// GoldConfig.Waves[currentWaveIndex].GoldPerEnemy at the moment the kill
// is registered. See WaveManager.HandleEnemyKilled.
/// 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 livesCost, bool flying)
{
pendingMaxHp = maxHp;
pendingLivesCost = livesCost;
pendingIsFlying = flying;
hasPendingInit = true;
// Cache locally on the server immediately — clients resolve via NV.
MaxHp = maxHp;
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;
}
public override void OnNetworkDespawn()
{
// If this enemy was the locally-selected ISelectable, clear the
// selection so the HUD doesn't keep displaying a stale corpse.
// SelectionState is a local UI singleton, safe to query on any peer.
var sel = SelectionState.Instance;
if (sel != null && sel.IsSelected(this))
sel.Clear();
}
// ----- 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()
{
// Fire OnDied immediately so the wave count decrements and gold is
// awarded the moment HP hits zero — the corpse animation and sink
// play out asynchronously after that.
OnDied?.Invoke(this);
// Stop pathing and remove from tower targeting / selection.
var movement = GetComponent();
if (movement != null) movement.enabled = false;
foreach (var col in GetComponentsInChildren())
col.enabled = false;
// Trigger the death animation on all peers (NetworkAnimator syncs it).
if (deathAnimator != null)
deathAnimator.SetTrigger(DieHash);
StartCoroutine(DeathSequence());
}
// ----- ISelectable ----------------------------------------------------
///
/// Enemy display name. Pulls from when assigned;
/// falls back to "Enemy" so the HUD never shows an empty portrait label
/// even if a prefab is missing its definition reference.
///
public string DisplayName =>
definition != null && !string.IsNullOrEmpty(definition.DisplayName)
? definition.DisplayName
: "Enemy";
public SelectableKind Kind => SelectableKind.Enemy;
public Transform SelectionTransform => transform;
// Used by SelectionVisualizer to size the selection ring. Tuned small
// because enemy capsules are roughly 0.6-0.8 wide; if you change enemy
// mesh sizes substantially, derive this from a collider bounds instead.
public float SelectionRadius => 0.4f;
private IEnumerator DeathSequence()
{
// Hold while the death animation plays.
yield return new WaitForSeconds(deathAnimationDuration);
// Sink phase. Server moves the transform; NetworkTransform replicates it.
float t = 0f;
Vector3 startPos = transform.position;
Vector3 endPos = startPos + Vector3.down * sinkDepth;
while (t < sinkDuration)
{
t += Time.deltaTime;
transform.position = Vector3.Lerp(startPos, endPos, t / sinkDuration);
yield return null;
}
if (NetworkObject != null && NetworkObject.IsSpawned)
NetworkObject.Despawn();
}
}
}