UnityTowerDefense/Assets/_Project/Scripts/UI/FloatingTextSpawner.cs
Matt F 4892d7253d First pass at full refactor to 2.0 design
Restructures the game around the cyclical run loop from Game Design Doc V2:
5 waves = a cycle, 3 cycles = a phase, each phase ends in a boss.

- New TD.Gameplay.Waves: WaveGroup / PhaseDefinition / RunDefinition author the
  run as draggable weighted pools; RunState owns phase/cycle position, the drawn
  wave slots, and the per-slot enemy buff sets. WaveManager's flat wave array is
  gone -- it now only runs the encounter RunState points at.
- New TD.Gameplay.EnemyUpgrades: the post-wave enemy-buff vote, with public
  live-replicated ballots so the HUD can show who voted for what.
- Inter-wave flow is now strictly sequential: draft -> vote -> build, each stage
  ending early once every player has acted.
- Enemy abilities inverted from per-instance random rolls to deterministic,
  stacking per-wave-slot sets. Six cards ship: Split (reworked), Flight, Blink,
  No Bounty, Gold Theft, Double Up.
- Tower upgrades are a two-step tree: a draft pick unlocks a node, gold converts
  an already-placed tower in place.
- Boss encounters flag their enemies and drive a boss HP bar.
- Player cap reduced to 3 via MatchRules.MaxPlayers.
- GoldConfig is now keyed by global encounter number rather than wave index.

Compiles clean; NOT yet verified in-engine. Editor wiring still required --
see Docs/2.0_Setup_Checklist.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:45:11 -07:00

112 lines
4.7 KiB
C#

// Assets/_Project/Scripts/UI/FloatingTextSpawner.cs
using UnityEngine;
namespace TD.UI
{
/// <summary>
/// Scene singleton that spawns <see cref="FloatingText"/> instances above
/// world positions for gold-reward and life-loss feedback.
/// </summary>
/// <remarks>
/// <b>Why a singleton:</b> the spawner is shared by every caller (kills, leaks,
/// future status pop-ups) and holds inspector-tunable colors and a prefab
/// reference. Plain <c>MonoBehaviour</c> — visual-only, no networking required.
///
/// <b>Who calls this:</b> <see cref="TD.Gameplay.WaveManager"/> invokes
/// <see cref="SpawnGoldReward"/> and <see cref="SpawnLifeLoss"/> via ClientRpc
/// so every peer shows the popup locally.
///
/// <b>Inspector setup:</b> drop this on any scene GameObject (e.g. a
/// <c>FloatingTextSpawner</c> empty), assign the floating-text prefab, tune
/// colors to match the HUD.
/// </remarks>
public class FloatingTextSpawner : MonoBehaviour
{
// ----- Singleton --------------------------------------------------
public static FloatingTextSpawner Instance { get; private set; }
// ----- Inspector --------------------------------------------------
[Tooltip("Prefab to instantiate for each pop-up. Must have a FloatingText " +
"component and a TextMeshPro renderer.")]
[SerializeField] private FloatingText prefab;
[Tooltip("Color used for kill-reward gold pop-ups. Tune to match the HUD's " +
"gold label color.")]
[SerializeField] private Color goldColor = new Color(1f, 0.84f, 0.2f);
[Tooltip("Color used for life-loss pop-ups (enemies reaching the goal).")]
[SerializeField] private Color livesColor = new Color(0.95f, 0.25f, 0.25f);
[Tooltip("Default vertical offset above the source position. Tunes how high " +
"above the enemy the text starts.")]
[SerializeField] private float verticalOffset = 2.0f;
[Tooltip("Maximum random horizontal jitter applied to the spawn position so " +
"consecutive pop-ups don't stack visually.")]
[SerializeField] private float positionJitter = 0.4f;
[Tooltip("Maximum random vertical jitter applied on top of verticalOffset.")]
[SerializeField] private float verticalJitter = 0.2f;
// ----- Lifecycle --------------------------------------------------
private void Awake()
{
if (Instance != null && Instance != this)
{
Debug.LogWarning("[FloatingTextSpawner] Duplicate instance detected. " +
"Only one should exist per scene.");
return;
}
Instance = this;
}
private void OnDestroy()
{
if (Instance == this) Instance = null;
}
// ----- Public API -------------------------------------------------
/// <summary>Spawns a gold-reward popup (e.g. "+10") at the given world position.</summary>
public void SpawnGoldReward(Vector3 worldPos, int amount)
=> SpawnInternal(worldPos, $"+{amount}", goldColor);
/// <summary>Spawns a gold-loss popup (e.g. "-15") at the given world position. Same colour
/// as a reward so it reads as gold; the sign carries the meaning.</summary>
public void SpawnGoldLoss(Vector3 worldPos, int amount)
=> SpawnInternal(worldPos, $"-{amount}", goldColor);
/// <summary>Spawns a life-loss popup (e.g. "-1") at the given world position.</summary>
public void SpawnLifeLoss(Vector3 worldPos, int amount)
=> SpawnInternal(worldPos, $"-{amount}", livesColor);
/// <summary>Lower-level overload: arbitrary content and color.</summary>
public void SpawnCustom(Vector3 worldPos, string text, Color color)
=> SpawnInternal(worldPos, text, color);
// ----- Internal ---------------------------------------------------
private void SpawnInternal(Vector3 worldPos, string text, Color color)
{
if (prefab == null)
{
Debug.LogWarning("[FloatingTextSpawner] No prefab assigned. " +
"Pop-up will not be shown.");
return;
}
Vector3 jitter = new Vector3(
Random.Range(-positionJitter, positionJitter),
Random.Range(0f, verticalJitter),
Random.Range(-positionJitter, positionJitter));
Vector3 spawnPos = worldPos + Vector3.up * verticalOffset + jitter;
var instance = Instantiate(prefab, spawnPos, Quaternion.identity);
instance.Init(text, color);
}
}
}