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

@ -4,6 +4,7 @@ using Unity.Netcode;
using UnityEngine;
using TD.Core;
using TD.Levels;
using TD.UI;
namespace TD.Gameplay
{
@ -66,10 +67,11 @@ namespace TD.Gameplay
// ----- Server-local runtime state ---------------------------------
private int remainingLives;
private int activeEnemyCount;
private bool spawningComplete;
private int currentWaveIndex = -1; // -1 = not yet started
private int remainingLives;
private int activeEnemyCount;
private bool spawningComplete;
private int currentWaveIndex = -1; // -1 = not yet started
private Coroutine activeWaveCoroutine;
// ----- NGO lifecycle ----------------------------------------------
@ -127,6 +129,13 @@ namespace TD.Gameplay
// ----- Public accessors -------------------------------------------
/// <summary>
/// Total number of waves in this match. Same on every peer because
/// <see cref="waveDefinitions"/> is a serialized prefab field, identical
/// on host and clients. Returns 0 if the array is unassigned.
/// </summary>
public int TotalWaves => waveDefinitions?.Length ?? 0;
/// <summary>
/// Number of times enemies have leaked out of the given player's zone.
/// Replicated — safe to call on any peer.
@ -148,7 +157,7 @@ namespace TD.Gameplay
// ----- Wave coroutine ---------------------------------------------
private void StartNextWave()
private void StartNextWave(bool skipPrep = false)
{
currentWaveIndex++;
@ -166,13 +175,63 @@ namespace TD.Gameplay
activeEnemyCount = 0;
spawningComplete = false;
StartCoroutine(RunWave(waveDefinitions[currentWaveIndex]));
activeWaveCoroutine = StartCoroutine(
RunWave(waveDefinitions[currentWaveIndex], skipPrep));
}
private IEnumerator RunWave(WaveDefinition def)
// ----- Dev / cheats -----------------------------------------------
/// <summary>
/// Dev cheat: skip the rest of the current wave (despawn any remaining
/// enemies, kill the prep timer) and start the next wave immediately.
/// Server-only — silently no-ops on clients. Safe to call during prep,
/// mid-spawn, or while enemies are alive.
/// </summary>
public void ForceAdvanceToNextWave()
{
// Prep phase — players build while the countdown ticks.
yield return new WaitForSeconds(def.PrepTime);
if (!IsServer) return;
// Stop the current wave's coroutine (cancels prep timer + remaining spawns).
if (activeWaveCoroutine != null)
{
StopCoroutine(activeWaveCoroutine);
activeWaveCoroutine = null;
}
// Despawn anything still running around. Iterate a snapshot since
// EnemyHealth.HandleDeath will despawn the NetworkObject, mutating
// the live collection.
var spawnManager = NetworkManager.Singleton?.SpawnManager;
if (spawnManager != null)
{
var snapshot = new System.Collections.Generic.List<NetworkObject>(
spawnManager.SpawnedObjectsList);
foreach (var no in snapshot)
{
if (no == null || !no.IsSpawned) continue;
var movement = no.GetComponent<EnemyMovement>();
if (movement == null) continue;
// Unsubscribe so the despawn doesn't deduct lives or fire side effects.
var h = no.GetComponent<EnemyHealth>();
UnsubscribeEnemy(h);
no.Despawn();
}
}
// Reset bookkeeping and start the next wave's RunWave coroutine,
// skipping the prep timer so spawning starts immediately.
activeEnemyCount = 0;
spawningComplete = false;
StartNextWave(skipPrep: true);
}
private IEnumerator RunWave(WaveDefinition def, bool skipPrep = false)
{
// Prep phase — players build while the countdown ticks. Skipped when
// a dev cheat forces the wave to start immediately.
if (!skipPrep)
yield return new WaitForSeconds(def.PrepTime);
// Spawn phase.
if (def.Entries != null)
@ -209,6 +268,13 @@ namespace TD.Gameplay
{
if (zone.Spawners == null || zone.Spawners.Length == 0) continue;
// Skip zones whose owning slot is empty. Until a lobby exists,
// this means a 1-player test session only spawns enemies in
// Player 1's zone; Player 2/3/... zones stay quiet until those
// slots are actually filled. PlayerMatchState.GetForSlot returns
// null for unoccupied slots (and for PlayerSlot.None).
if (PlayerMatchState.GetForSlot(zone.Owner) == null) continue;
// Use the first spawner in the zone. Future: round-robin through Spawners.
SpawnEnemy(def, zone.Spawners[0].TilePosition);
}
@ -264,6 +330,13 @@ namespace TD.Gameplay
?.AwardGold(health.GoldReward);
}
// Show a "+N" gold popup above the corpse on every peer. Capture the
// position here on the server — by the time the RPC fires on clients
// the death sequence will be moving the corpse, but the spawn point
// is good enough and we want the popup to anchor where the kill happened.
if (health.GoldReward > 0)
ShowGoldRewardClientRpc(health.transform.position, health.GoldReward);
UnsubscribeEnemy(health);
DecrementAndCheckComplete();
}
@ -278,6 +351,13 @@ namespace TD.Gameplay
private void HandleEnemyReachedGoal(EnemyMovement movement, int livesCost)
{
// Capture the leak position BEFORE the enemy NetworkObject despawns
// (HandleGoalReached on the enemy calls Despawn right after firing the
// event we're handling here). Show the "-N" popup on every peer.
Vector3 leakPos = movement.transform.position;
if (livesCost > 0)
ShowLifeLossClientRpc(leakPos, livesCost);
UnsubscribeEnemy(movement.GetComponent<EnemyHealth>());
remainingLives = Mathf.Max(0, remainingLives - livesCost);
@ -293,6 +373,22 @@ namespace TD.Gameplay
DecrementAndCheckComplete();
}
// ----- Floating-text ClientRpcs -----------------------------------
// Fired on every peer (server + clients) so each one spawns its own local
// FloatingText. The spawned GameObjects are not networked — purely visual.
[ClientRpc]
private void ShowGoldRewardClientRpc(Vector3 worldPos, int amount)
{
FloatingTextSpawner.Instance?.SpawnGoldReward(worldPos, amount);
}
[ClientRpc]
private void ShowLifeLossClientRpc(Vector3 worldPos, int amount)
{
FloatingTextSpawner.Instance?.SpawnLifeLoss(worldPos, amount);
}
// ----- Helpers ----------------------------------------------------
private void UnsubscribeEnemy(EnemyHealth health)