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>
116 lines
4.5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|