UnityTowerDefense/Assets/_Project/Scripts/Gameplay/PlayerTowerUpgrades.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

116 lines
4.5 KiB
C#

// Assets/_Project/Scripts/Gameplay/PlayerTowerUpgrades.cs
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
namespace TD.Gameplay
{
/// <summary>
/// Per-player set of unlocked tower <b>upgrade nodes</b> — the tower types this player is
/// allowed to convert an existing tower into. Lives on the Player prefab alongside
/// <see cref="PlayerTowerDeck"/>.
/// </summary>
/// <remarks>
/// <para><b>Distinct from the deck, deliberately.</b> <see cref="PlayerTowerDeck"/> is what you
/// can <i>build from scratch</i>; this is what you can <i>upgrade into</i>. They have to be
/// separate sets because the 2.0 model is two-step: a draft pick unlocks a node, and gold spent
/// on an already-placed tower converts it. Unlocking an upgrade must not make it directly
/// buildable, or the gold step — the entire cost of the upgrade — is bypassed.</para>
///
/// <para><b>The tree lives in the data.</b> Edges are <c>TowerDefinition.UpgradePaths</c>; this
/// component only records which nodes a player has earned the right to use. Whether a
/// particular tower can take a particular upgrade is a question for the tree (is it a child of
/// what's placed?) crossed with this set (has the player unlocked it?).</para>
///
/// <para>Append-only within a match, mirroring the deck. Reset happens by re-initializing at
/// match start, so Retry / return-to-lobby cycles start clean.</para>
/// </remarks>
public class PlayerTowerUpgrades : NetworkBehaviour
{
// ----- Static registry (mirrors PlayerTowerDeck / PlayerGoldManager) -----
private static readonly Dictionary<ulong, PlayerTowerUpgrades> s_byClientId
= new Dictionary<ulong, PlayerTowerUpgrades>();
public static PlayerTowerUpgrades GetForClient(ulong clientId)
{
s_byClientId.TryGetValue(clientId, out var upgrades);
return upgrades;
}
public static PlayerTowerUpgrades Local
{
get
{
var nm = NetworkManager.Singleton;
if (nm == null || !nm.IsClient) return null;
return GetForClient(nm.LocalClientId);
}
}
// ----- Networked state --------------------------------------------
// TowerTypeIds (catalog indices) this player may upgrade into.
private NetworkList<int> unlockedNodes;
/// <summary>Fired on every peer when the unlocked set changes. The HUD subscribes so a
/// selected tower's upgrade buttons appear the moment a draft grants them.</summary>
public event System.Action OnUpgradesChanged;
private void Awake()
{
unlockedNodes = new NetworkList<int>();
}
public override void OnNetworkSpawn()
{
s_byClientId[OwnerClientId] = this;
unlockedNodes.OnListChanged += HandleListChanged;
}
public override void OnNetworkDespawn()
{
unlockedNodes.OnListChanged -= HandleListChanged;
if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
s_byClientId.Remove(OwnerClientId);
}
private void HandleListChanged(NetworkListEvent<int> _) => OnUpgradesChanged?.Invoke();
// ----- Read API ---------------------------------------------------
public int Count => unlockedNodes.Count;
public int GetTypeIdAt(int index) => unlockedNodes[index];
/// <summary>True if this player has unlocked the given upgrade node.</summary>
public bool Contains(int towerTypeId)
{
for (int i = 0; i < unlockedNodes.Count; i++)
if (unlockedNodes[i] == towerTypeId) return true;
return false;
}
// ----- Server API -------------------------------------------------
/// <summary>Server-only: clear the unlocked set. Called at match start.</summary>
public void ServerInitialize()
{
if (!IsServer) return;
unlockedNodes.Clear();
}
/// <summary>
/// Server-only: unlock an upgrade node. Returns false if already unlocked (so a draft
/// option can report a wasted pick rather than silently succeeding).
/// </summary>
public bool ServerUnlock(int towerTypeId)
{
if (!IsServer) return false;
if (towerTypeId < 0 || Contains(towerTypeId)) return false;
unlockedNodes.Add(towerTypeId);
return true;
}
}
}