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>
This commit is contained in:
Matt F 2026-07-30 17:45:11 -07:00
parent 7e5c3a8279
commit 4892d7253d
64 changed files with 4023 additions and 344 deletions

View file

@ -1,5 +1,6 @@
// Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using TD.Core;
@ -67,6 +68,7 @@ namespace TD.Gameplay
private int pendingLivesCost = 1;
private bool pendingIsFlying;
private bool pendingIsHeld;
private bool pendingIsBoss;
private bool hasPendingInit;
// ----- Server-local runtime state -------------------------------------
@ -103,6 +105,13 @@ namespace TD.Gameplay
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
// Set for enemies spawned by a phase's boss encounter. Replicated because the HUD's boss
// bar is client-side and has to recognise a boss without asking the server.
private readonly NetworkVariable<bool> isBoss = new NetworkVariable<bool>(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
// ----- Public state ---------------------------------------------------
public float CurrentHp => hp.Value;
@ -117,6 +126,19 @@ namespace TD.Gameplay
/// </summary>
public bool IsFlying => isFlying.Value;
/// <summary>True if this enemy was spawned by a phase's boss encounter. Replicated.</summary>
public bool IsBoss => isBoss.Value;
/// <summary>
/// Every live boss, on every peer. Maintained by spawn/despawn so the HUD can render a
/// boss bar without polling the scene.
/// </summary>
/// <remarks>
/// A list rather than a single reference because the boss encounter spawns one boss per
/// player zone — there are as many simultaneous bosses as there are players.
/// </remarks>
public static readonly List<EnemyHealth> ActiveBosses = new List<EnemyHealth>();
// ----- Events ---------------------------------------------------------
/// <summary>
@ -136,12 +158,14 @@ namespace TD.Gameplay
/// and before <c>NetworkObject.Spawn()</c>. Mirrors the
/// <c>TowerInstance.InitializeServer</c> pattern.
/// </summary>
public void InitializeServer(float maxHp, int livesCost, bool flying, bool held = false)
public void InitializeServer(float maxHp, int livesCost, bool flying, bool held = false,
bool boss = false)
{
pendingMaxHp = maxHp;
pendingLivesCost = livesCost;
pendingIsFlying = flying;
pendingIsHeld = held;
pendingIsBoss = boss;
hasPendingInit = true;
// Cache locally on the server immediately — clients resolve via NV.
@ -158,6 +182,7 @@ namespace TD.Gameplay
hp.Value = pendingMaxHp;
isFlying.Value = pendingIsFlying;
isHeld.Value = pendingIsHeld;
isBoss.Value = pendingIsBoss;
hasPendingInit = false;
}
@ -165,6 +190,9 @@ namespace TD.Gameplay
if (!IsServer)
MaxHp = hp.Value;
if (isBoss.Value && !ActiveBosses.Contains(this))
ActiveBosses.Add(this);
// Apply initial held state on all peers and watch for future changes.
// isHeld.OnValueChanged doesn't fire for the initial replication, so we
// apply it explicitly here as well.
@ -174,6 +202,8 @@ namespace TD.Gameplay
public override void OnNetworkDespawn()
{
ActiveBosses.Remove(this);
// 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.