175 lines
7.1 KiB
C#
175 lines
7.1 KiB
C#
// Assets/_Project/Scripts/Gameplay/PlayerTowerDeck.cs
|
|
using System.Collections.Generic;
|
|
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
namespace TD.Gameplay
|
|
{
|
|
/// <summary>
|
|
/// Per-player set of unlocked tower types — the player's "deck". Lives on the
|
|
/// Player prefab alongside <see cref="PlayerGoldManager"/> and
|
|
/// <see cref="PlayerMatchState"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para><b>Roguelike keystone.</b> 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.</para>
|
|
///
|
|
/// <para><b>Identity.</b> The deck stores <c>TowerTypeId</c>s — indices into
|
|
/// <see cref="TowerPlacementManager"/>'s catalog (<c>towerDefinitions[]</c>), the
|
|
/// same identifier the placement RPC, <see cref="Builder"/>, and
|
|
/// <see cref="BuildJob"/> already use. No new network identifier is introduced.</para>
|
|
///
|
|
/// <para><b>Authority.</b> Server-only mutation (<see cref="ServerInitialize"/> /
|
|
/// <see cref="ServerGrantTower"/>). The owning client reads its own deck to build
|
|
/// the HUD command grid; the server reads it to gate placement
|
|
/// (<see cref="TowerPlacementManager"/> rejects a requested tower that isn't in the
|
|
/// requesting player's deck — the anti-cheat seam).</para>
|
|
///
|
|
/// <para><b>Persistence caveat.</b> <c>TowerTypeId</c> 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 <c>TowerDefinition</c>).</para>
|
|
/// </remarks>
|
|
public class PlayerTowerDeck : NetworkBehaviour
|
|
{
|
|
// ----- Static registry (mirrors PlayerGoldManager) ----------------
|
|
|
|
private static readonly Dictionary<ulong, PlayerTowerDeck> s_byClientId
|
|
= new Dictionary<ulong, PlayerTowerDeck>();
|
|
|
|
/// <summary>Returns the deck owned by the given client, or null.</summary>
|
|
public static PlayerTowerDeck GetForClient(ulong clientId)
|
|
{
|
|
s_byClientId.TryGetValue(clientId, out var deck);
|
|
return deck;
|
|
}
|
|
|
|
/// <summary>Convenience: the local client's own deck. Null on a dedicated
|
|
/// server or before the local player has spawned.</summary>
|
|
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<int> unlockedTypeIds;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public event System.Action OnDeckChanged;
|
|
|
|
private void Awake()
|
|
{
|
|
unlockedTypeIds = new NetworkList<int>();
|
|
}
|
|
|
|
// ----- 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<int> _) => OnDeckChanged?.Invoke();
|
|
|
|
// ----- Read API ---------------------------------------------------
|
|
|
|
/// <summary>Number of towers unlocked in this deck.</summary>
|
|
public int Count => unlockedTypeIds.Count;
|
|
|
|
/// <summary>The TowerTypeId at the given deck slot (0..Count-1).</summary>
|
|
public int GetTypeIdAt(int index) => unlockedTypeIds[index];
|
|
|
|
/// <summary>True if the given TowerTypeId is unlocked in this deck.</summary>
|
|
public bool Contains(int towerTypeId)
|
|
{
|
|
for (int i = 0; i < unlockedTypeIds.Count; i++)
|
|
if (unlockedTypeIds[i] == towerTypeId) return true;
|
|
return false;
|
|
}
|
|
|
|
// ----- Server mutation --------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Server-only: replace the deck with the given starting set. Called at match
|
|
/// start (<see cref="WaveManager"/>) so Retry / return-to-lobby cycles reset
|
|
/// the deck cleanly. Ignores ids <= 0 (the reserved catalog index) and
|
|
/// silently de-dupes.
|
|
/// </summary>
|
|
public void ServerInitialize(IReadOnlyList<int> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public bool ServerGrantTower(int towerTypeId)
|
|
{
|
|
if (!IsServer) return false;
|
|
if (towerTypeId <= 0) return false;
|
|
if (Contains(towerTypeId)) return false;
|
|
|
|
unlockedTypeIds.Add(towerTypeId);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|