Adding Text Mesh Pro, new enemies, new enemy waves, new HUD functionality, New animators, a retry function, and floating text for gold earned and lives lost.

This commit is contained in:
Matt F 2026-05-13 17:39:16 -07:00
parent 3287e8ea43
commit f6cc6a7102
110 changed files with 62003 additions and 251 deletions

View file

@ -1,4 +1,5 @@
// Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
using System.Collections;
using Unity.Netcode;
using UnityEngine;
using TD.Core;
@ -19,13 +20,47 @@ namespace TD.Gameplay
/// 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.
/// <b>Death flow (server-only):</b> <see cref="TakeDamage"/> clamps HP to 0
/// and fires <see cref="OnDied"/> immediately so wave bookkeeping and gold
/// award happen the moment HP hits zero. The component then disables
/// <see cref="EnemyMovement"/> and all child <c>Collider</c>s, triggers the
/// <c>Die</c> animator parameter (synced via <c>NetworkAnimator</c>), waits
/// <see cref="deathAnimationDuration"/> seconds, sinks the transform
/// <see cref="sinkDepth"/> units over <see cref="sinkDuration"/> seconds,
/// and finally calls <c>NetworkObject.Despawn</c>.
/// </remarks>
[RequireComponent(typeof(NetworkObject))]
public class EnemyHealth : NetworkBehaviour
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;
/// <summary>The static definition this enemy was spawned from.</summary>
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;
@ -121,6 +156,16 @@ namespace TD.Gameplay
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 -----------------------------------------------------
/// <summary>
@ -148,8 +193,64 @@ namespace TD.Gameplay
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);
NetworkObject.Despawn();
// Stop pathing and remove from tower targeting / selection.
var movement = GetComponent<EnemyMovement>();
if (movement != null) movement.enabled = false;
foreach (var col in GetComponentsInChildren<Collider>())
col.enabled = false;
// Trigger the death animation on all peers (NetworkAnimator syncs it).
if (deathAnimator != null)
deathAnimator.SetTrigger(DieHash);
StartCoroutine(DeathSequence());
}
// ----- ISelectable ----------------------------------------------------
/// <summary>
/// Enemy display name. Pulls from <see cref="definition"/> when assigned;
/// falls back to "Enemy" so the HUD never shows an empty portrait label
/// even if a prefab is missing its definition reference.
/// </summary>
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();
}
}
}