// 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;
}
///
/// Server-only: replaces a previously unlocked tower type with a different one (an
/// upgrade pick) — every tower of that type placed from now on uses the new definition.
/// Already-placed towers keep whatever type they were placed with; this only changes
/// what's placeable going forward. No-op if the old type isn't currently unlocked or
/// the new one already is. Returns true if the swap happened.
///
public bool ServerUpgradeTower(int oldTypeId, int newTypeId)
{
if (!IsServer) return false;
if (newTypeId <= 0) return false;
if (Contains(newTypeId)) return false;
for (int i = 0; i < unlockedTypeIds.Count; i++)
{
if (unlockedTypeIds[i] == oldTypeId)
{
unlockedTypeIds.RemoveAt(i);
unlockedTypeIds.Add(newTypeId);
return true;
}
}
return false;
}
}
}