// 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; /// /// 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; } } }