// Assets/_Project/Scripts/Gameplay/PlayerMatchState.cs
using System.Collections.Generic;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using TD.Core;
namespace TD.Gameplay
{
///
/// Per-player match state. One instance is spawned per connected client (via
/// ) and owned by that client.
///
/// Mirrors the pattern: static registry keyed by
/// OwnerClientId, server-authoritative NetworkVariables, and a
/// Local convenience accessor.
///
/// The server assigns a on spawn using the next free slot
/// (Player1..Player9 in connect order). All five former STUB mappings
/// (Builder, TowerPlacementManager, TowerPlacementController, CameraController,
/// PlayerBuilderSpawner) delegate here instead.
///
public class PlayerMatchState : NetworkBehaviour
{
// --- Static registry ---------------------------------------------
private static readonly Dictionary s_byClientId
= new Dictionary();
// Tracks which slots are currently occupied so the server can assign the next free one.
private static readonly HashSet s_assignedSlots = new HashSet();
///
/// Returns the for the given client, or null if not spawned.
/// Safe to call on server or client.
///
public static PlayerMatchState GetForClient(ulong clientId)
{
s_byClientId.TryGetValue(clientId, out var state);
return state;
}
///
/// Returns the for the given client, or
/// if the client has no spawned .
/// Convenience wrapper over .
///
public static PlayerSlot SlotForClient(ulong clientId)
=> GetForClient(clientId)?.Slot ?? PlayerSlot.None;
///
/// Returns the whose assigned slot matches
/// , or null if no connected client holds that slot.
/// O(n) over connected players (max 9) — acceptable for server-side use.
/// Used by WaveManager to resolve kill-gold recipients.
///
public static PlayerMatchState GetForSlot(PlayerSlot slot)
{
foreach (var pms in s_byClientId.Values)
if (pms.Slot == slot) return pms;
return null;
}
///
/// The local client's own state. Null on a dedicated server or before the
/// local player has spawned.
///
public static PlayerMatchState Local
{
get
{
var nm = NetworkManager.Singleton;
if (nm == null || !nm.IsClient) return null;
return GetForClient(nm.LocalClientId);
}
}
// --- Networked state ---------------------------------------------
private readonly NetworkVariable slot = new NetworkVariable(
value: PlayerSlot.None,
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Server
);
private readonly NetworkVariable displayName = new NetworkVariable(
value: default,
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Server
);
// STUBBED — written by the race-pick system (Phase 1.8). None until a race is chosen.
[Tooltip("STUBBED — not written until the race-pick system lands (Phase 1.8).")]
private readonly NetworkVariable raceSelection = new NetworkVariable(
value: RaceId.None,
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Server
);
/// This player's assigned slot in the match. Authoritative once spawned.
public PlayerSlot Slot => slot.Value;
/// Display name. Stub until a lobby/name system provides it.
public string DisplayName => displayName.Value.ToString();
/// STUBBED. Race chosen by this player. until Phase 1.8.
public RaceId RaceSelection => raceSelection.Value;
/// STUBBED. Sets this player's race selection. Called by the race-pick system (Phase 1.8).
public void SetRaceSelection(RaceId race)
{
if (!IsServer) { Debug.LogWarning("[PlayerMatchState] SetRaceSelection called on a client — ignored."); return; }
raceSelection.Value = race;
}
///
/// Server-only. Fires on the server immediately after the slot is assigned in
/// . Sibling components (e.g. PlayerBuilderSpawner)
/// subscribe to defer work that requires a valid slot.
///
public event System.Action SlotReady;
// --- Lifecycle ---------------------------------------------------
public override void OnNetworkSpawn()
{
s_byClientId[OwnerClientId] = this;
if (IsServer)
{
PlayerSlot assigned = AllocateNextSlot();
slot.Value = assigned;
displayName.Value = new FixedString32Bytes($"Player {(int)assigned}");
s_assignedSlots.Add(assigned);
Debug.Log($"[PlayerMatchState] Assigned slot {assigned} to client {OwnerClientId}.");
SlotReady?.Invoke(assigned);
}
}
public override void OnNetworkDespawn()
{
if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this)
s_byClientId.Remove(OwnerClientId);
if (IsServer)
s_assignedSlots.Remove(slot.Value);
}
// --- Slot allocation (server-only) -------------------------------
private static PlayerSlot AllocateNextSlot()
{
for (int i = 1; i <= 9; i++)
{
var candidate = (PlayerSlot)i;
if (!s_assignedSlots.Contains(candidate))
return candidate;
}
Debug.LogError("[PlayerMatchState] No free PlayerSlot (already 9 players). " +
"Returning None — this client will be unable to play.");
return PlayerSlot.None;
}
}
}