// Assets/_Project/Scripts/Gameplay/PlayerTowerDeck.cs using System.Collections.Generic; using Unity.Netcode; using UnityEngine; namespace TD.Gameplay { /// /// Per-player set of unlocked tower types — the player's "deck". Lives on the /// Player prefab alongside , /// , and . /// /// /// Roguelike keystone. Players no longer see every tower in the match /// catalog. Each starts with a small base set (the starter towers) and grows the /// deck through draft choices. This component is the data structure that growth /// targets. /// /// Identity. The deck stores TowerTypeIds — indices into /// 's catalog (towerDefinitions[]), the /// same identifier the placement RPC, , and /// already use. No new network identifier is introduced. /// /// Authority. Server-only mutation ( / /// ). The owning client reads its own deck to build /// the HUD command grid; the server reads it to gate placement /// ( rejects a requested tower that isn't in the /// requesting player's deck — the anti-cheat seam). /// /// Persistence caveat. TowerTypeId is a catalog index — stable /// within a single match but NOT across sessions. Cross-match deck persistence (a /// later keystone) will need a stable per-tower id (asset GUID or a serialized /// StableId on TowerDefinition). /// public class PlayerTowerDeck : NetworkBehaviour { // ----- Static registry (mirrors PlayerGoldManager) ---------------- private static readonly Dictionary s_byClientId = new Dictionary(); /// Returns the deck owned by the given client, or null. public static PlayerTowerDeck GetForClient(ulong clientId) { s_byClientId.TryGetValue(clientId, out var deck); return deck; } /// Convenience: the local client's own deck. Null on a dedicated /// server or before the local player has spawned. public static PlayerTowerDeck Local { get { var nm = NetworkManager.Singleton; if (nm == null || !nm.IsClient) return null; return GetForClient(nm.LocalClientId); } } // ----- Networked state -------------------------------------------- // Unlocked tower type ids (indices into TowerPlacementManager's catalog). // readPerm defaults to Everyone (consistent with the other per-player // managers); only the owner consumes it for the HUD. writePerm Server. private NetworkList unlockedTypeIds; /// /// Fired on every peer when the deck contents change (server init, tower /// granted). The HUD subscribes to rebuild the command grid live when a draft /// grant lands while the builder is already selected. /// public event System.Action OnDeckChanged; private void Awake() { unlockedTypeIds = new NetworkList(); } // ----- NGO lifecycle ---------------------------------------------- public override void OnNetworkSpawn() { s_byClientId[OwnerClientId] = this; unlockedTypeIds.OnListChanged += HandleListChanged; } public override void OnNetworkDespawn() { unlockedTypeIds.OnListChanged -= HandleListChanged; if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this) s_byClientId.Remove(OwnerClientId); } private void HandleListChanged(NetworkListEvent _) => OnDeckChanged?.Invoke(); // ----- Read API --------------------------------------------------- /// Number of towers unlocked in this deck. public int Count => unlockedTypeIds.Count; /// The TowerTypeId at the given deck slot (0..Count-1). public int GetTypeIdAt(int index) => unlockedTypeIds[index]; /// True if the given TowerTypeId is unlocked in this deck. public bool Contains(int towerTypeId) { for (int i = 0; i < unlockedTypeIds.Count; i++) if (unlockedTypeIds[i] == towerTypeId) return true; return false; } // ----- Server mutation -------------------------------------------- /// /// Server-only: replace the deck with the given starting set. Called at match /// start () so Retry / return-to-lobby cycles reset /// the deck cleanly. Ignores ids <= 0 (the reserved catalog index) and /// silently de-dupes. /// public void ServerInitialize(IReadOnlyList startingTypeIds) { if (!IsServer) return; unlockedTypeIds.Clear(); if (startingTypeIds == null) return; for (int i = 0; i < startingTypeIds.Count; i++) { int id = startingTypeIds[i]; if (id > 0 && !Contains(id)) unlockedTypeIds.Add(id); } } /// /// Server-only: add a tower type to this deck (a draft grant). No-op if the id /// is invalid or already present. Returns true if it was actually added. /// public bool ServerGrantTower(int towerTypeId) { if (!IsServer) return false; if (towerTypeId <= 0) return false; if (Contains(towerTypeId)) return false; unlockedTypeIds.Add(towerTypeId); return true; } } }