Adding Match State controller
This commit is contained in:
parent
c100db52e5
commit
abcefcd7f1
13 changed files with 445 additions and 99 deletions
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(grep -E \"\\\\.\\(prefab|fbx|controller\\)$\")",
|
||||
"Bash(dir /s /b \"C:\\\\Users\\\\catos\\\\UnityTowerDefense\\\\*.md\")",
|
||||
"Bash(findstr /v \"Library\\\\PackageCache\")",
|
||||
"Bash(dir \"C:\\\\Users\\\\catos\\\\UnityTowerDefense\" /a-d)",
|
||||
"Bash(mkdir -p \"C:\\\\Users\\\\catos\\\\UnityTowerDefense\\\\Assets\\\\_Project\\\\UI\")",
|
||||
"Bash(mkdir -p \"C:\\\\Users\\\\catos\\\\UnityTowerDefense\\\\Assets\\\\_Project\\\\Scripts\\\\UI\")",
|
||||
"Bash(mkdir -p \"C:/Users/catos/UnityTowerDefense/Assets/_Project/Scripts/UI/Minimap\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ GameObject:
|
|||
- component: {fileID: 2152427255203126265}
|
||||
- component: {fileID: 2918837822014987993}
|
||||
- component: {fileID: 7845089877743661692}
|
||||
- component: {fileID: 4336209376377567030}
|
||||
m_Layer: 0
|
||||
m_Name: Player
|
||||
m_TagString: Untagged
|
||||
|
|
@ -87,3 +88,16 @@ MonoBehaviour:
|
|||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerBuilderSpawner
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
builderPrefab: {fileID: 116861493430507844, guid: 3398cc5831880954487717577f61b6d7, type: 3}
|
||||
--- !u!114 &4336209376377567030
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3493329038866903420}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4ce7a1bcbbd94474d9f5bc855c2394fc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerMatchState
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
|
|
|
|||
|
|
@ -906,6 +906,77 @@ Transform:
|
|||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &902199259
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 902199262}
|
||||
- component: {fileID: 902199260}
|
||||
- component: {fileID: 902199261}
|
||||
m_Layer: 0
|
||||
m_Name: MatchState
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &902199260
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 902199259}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||
GlobalObjectIdHash: 1302863789
|
||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
AlwaysReplicateAsRoot: 0
|
||||
SynchronizeTransform: 1
|
||||
ActiveSceneSynchronization: 0
|
||||
SceneMigrationSynchronization: 0
|
||||
SpawnWithObservers: 1
|
||||
DontDestroyWithOwner: 0
|
||||
AutoObjectParentSync: 1
|
||||
SyncOwnerTransformWhenParented: 1
|
||||
AllowOwnerToParent: 0
|
||||
--- !u!114 &902199261
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 902199259}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a34585be59d49e94b811d96be51c5088, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.MatchState
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
--- !u!4 &902199262
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 902199259}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 19.73428, y: 12.20541, z: 11.3821}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &923592498
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -2231,3 +2302,4 @@ SceneRoots:
|
|||
- {fileID: 1222526238}
|
||||
- {fileID: 1058315976}
|
||||
- {fileID: 1168515846}
|
||||
- {fileID: 902199262}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,38 @@ namespace TD.Core
|
|||
/// Player1..Player9 cover the maximum supported player count. Maps using fewer players use a
|
||||
/// contiguous prefix (e.g., a 3-player map uses Player1, Player2, Player3 only).
|
||||
/// </remarks>
|
||||
/// <summary>
|
||||
/// Global phase of a match, driven by <c>MatchState</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Transitions are server-authoritative. Clients react to
|
||||
/// <c>NetworkVariable<MatchPhase>.OnValueChanged</c>.
|
||||
/// </remarks>
|
||||
public enum MatchPhase : byte
|
||||
{
|
||||
/// <summary>Pre-match; players are connecting and the server hasn't started the countdown.</summary>
|
||||
Lobby = 0,
|
||||
/// <summary>Brief count-down before waves begin. Placement is still allowed.</summary>
|
||||
CountDown = 1,
|
||||
/// <summary>Waves are in progress; normal gameplay.</summary>
|
||||
Playing = 2,
|
||||
/// <summary>All waves cleared; co-op win state.</summary>
|
||||
Victory = 3,
|
||||
/// <summary>All lives lost; co-op defeat state.</summary>
|
||||
Defeat = 4,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the race a player has chosen in the race-pick phase.
|
||||
/// Backed by byte. Specific race values are defined in Phase 1.8.
|
||||
/// </summary>
|
||||
public enum RaceId : byte
|
||||
{
|
||||
/// <summary>No race selected yet (lobby / pre-pick).</summary>
|
||||
None = 0,
|
||||
// Race entries added in Phase 1.8.
|
||||
}
|
||||
|
||||
public enum PlayerSlot : byte
|
||||
{
|
||||
None = 0,
|
||||
|
|
|
|||
|
|
@ -268,23 +268,13 @@ namespace TD.Gameplay
|
|||
|
||||
// ----- IMinimapEntity ---------------------------------------------
|
||||
//
|
||||
// Read every minimap refresh tick. Position is read live (NetworkTransform
|
||||
// interpolation handles remote-client smoothing). Color comes from the same
|
||||
// OwnerClientId → PlayerSlot stub mapping used by ApplyOwnerColor; both will pick
|
||||
// up the real mapping when MatchState lands.
|
||||
// Position is read live (NetworkTransform interpolation handles remote-client smoothing).
|
||||
// Color comes from PlayerMatchState slot assignment.
|
||||
|
||||
Vector3 IMinimapEntity.WorldPosition => transform.position;
|
||||
|
||||
Color IMinimapEntity.MinimapColor
|
||||
{
|
||||
get
|
||||
{
|
||||
byte slotByte = (byte)(OwnerClientId + 1);
|
||||
PlayerSlot slot = (slotByte >= 1 && slotByte <= 9)
|
||||
? (PlayerSlot)slotByte : PlayerSlot.None;
|
||||
return PlayerColors.Get(slot);
|
||||
}
|
||||
}
|
||||
=> PlayerColors.Get(PlayerMatchState.SlotForClient(OwnerClientId));
|
||||
|
||||
MinimapIconKind IMinimapEntity.IconKind => MinimapIconKind.Builder;
|
||||
|
||||
|
|
@ -305,12 +295,7 @@ namespace TD.Gameplay
|
|||
|
||||
private void ApplyOwnerColor()
|
||||
{
|
||||
// Owner color comes from the slot mapping. Same stub mapping as elsewhere —
|
||||
// replaced when MatchState lands.
|
||||
byte slotByte = (byte)(OwnerClientId + 1);
|
||||
PlayerSlot slot = (slotByte >= 1 && slotByte <= 9) ? (PlayerSlot)slotByte : PlayerSlot.None;
|
||||
|
||||
Color c = PlayerColors.Get(slot);
|
||||
Color c = PlayerColors.Get(PlayerMatchState.SlotForClient(OwnerClientId));
|
||||
c.a = 1f;
|
||||
|
||||
colorPropertyBlock ??= new MaterialPropertyBlock();
|
||||
|
|
@ -560,12 +545,7 @@ namespace TD.Gameplay
|
|||
return;
|
||||
}
|
||||
|
||||
// Owner slot: same stub mapping as elsewhere.
|
||||
byte slotByte = (byte)(OwnerClientId + 1);
|
||||
PlayerSlot owner = (slotByte >= 1 && slotByte <= 9)
|
||||
? (PlayerSlot)slotByte
|
||||
: PlayerSlot.None;
|
||||
|
||||
PlayerSlot owner = PlayerMatchState.SlotForClient(OwnerClientId);
|
||||
visual.InitializeServer(def, owner, job.Anchor, job.TowerTypeId, job.GoldSpent);
|
||||
|
||||
var netObj = go.GetComponent<NetworkObject>();
|
||||
|
|
@ -1078,12 +1058,7 @@ namespace TD.Gameplay
|
|||
// ----- Helpers ----------------------------------------------------
|
||||
|
||||
private static PlayerSlot OwnerToSlot(ulong clientId)
|
||||
{
|
||||
// STUB — replaced when MatchState lands. Same mapping as elsewhere.
|
||||
byte slotByte = (byte)(clientId + 1);
|
||||
if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
|
||||
return (PlayerSlot)slotByte;
|
||||
}
|
||||
=> PlayerMatchState.SlotForClient(clientId);
|
||||
|
||||
private static Vector2Int ResolveFootprint(int towerTypeId)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -498,19 +498,7 @@ namespace TD.Gameplay
|
|||
|
||||
// ----- Player slot ------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns the local player's PlayerSlot.
|
||||
/// STUB: same trivial mapping used elsewhere; replaced when MatchState lands.
|
||||
/// </summary>
|
||||
private static PlayerSlot GetLocalPlayerSlot()
|
||||
{
|
||||
var nm = Unity.Netcode.NetworkManager.Singleton;
|
||||
if (nm == null || !nm.IsClient) return PlayerSlot.None;
|
||||
|
||||
ulong clientId = nm.LocalClientId;
|
||||
byte slotByte = (byte)(clientId + 1);
|
||||
if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
|
||||
return (PlayerSlot)slotByte;
|
||||
}
|
||||
=> PlayerMatchState.Local?.Slot ?? PlayerSlot.None;
|
||||
}
|
||||
}
|
||||
138
Assets/_Project/Scripts/Gameplay/MatchState.cs
Normal file
138
Assets/_Project/Scripts/Gameplay/MatchState.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// Assets/_Project/Scripts/Gameplay/MatchState.cs
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton NetworkBehaviour that tracks global match data.
|
||||
/// Lives on a persistent scene object.
|
||||
///
|
||||
/// All state is server-authoritative (Server-write NetworkVariables).
|
||||
/// Clients react to OnValueChanged events on individual fields.
|
||||
/// Phase transitions (Lobby → CountDown → Playing → Victory/Defeat)
|
||||
/// and wave/lives logic are wired in Phases 1.4–1.6 and 1.8.
|
||||
/// </summary>
|
||||
public class MatchState : NetworkBehaviour
|
||||
{
|
||||
// --- Singleton ---------------------------------------------------
|
||||
|
||||
public static MatchState Instance { get; private set; }
|
||||
|
||||
// --- Networked state ---------------------------------------------
|
||||
|
||||
private readonly NetworkVariable<MatchPhase> phase = new NetworkVariable<MatchPhase>(
|
||||
value: MatchPhase.Playing,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
/// <summary>Current match phase. Authoritative on the server, replicated to all clients.</summary>
|
||||
public MatchPhase Phase => phase.Value;
|
||||
|
||||
// STUBBED — written by the wave system (Phase 1.5/1.6). Zero until waves begin.
|
||||
[Tooltip("STUBBED — not written until the wave system lands (Phase 1.5/1.6).")]
|
||||
private readonly NetworkVariable<int> currentWave = new NetworkVariable<int>(
|
||||
value: 0,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
/// <summary>STUBBED. Current wave index. Zero until the wave system (Phase 1.5/1.6) writes it.</summary>
|
||||
public int CurrentWave => currentWave.Value;
|
||||
|
||||
// STUBBED — written by the combat/leak system (Phase 1.4). Zero until then.
|
||||
[Tooltip("STUBBED — not written until the combat/leak system lands (Phase 1.4).")]
|
||||
private readonly NetworkVariable<int> lives = new NetworkVariable<int>(
|
||||
value: 0,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
/// <summary>STUBBED. Shared lives pool. Zero until the combat/leak system (Phase 1.4) writes it.</summary>
|
||||
public int Lives => lives.Value;
|
||||
|
||||
// STUBBED — driven by the race-pick UI (Phase 1.8). Zero when no pick is in progress.
|
||||
[Tooltip("STUBBED — not driven until the race-pick system lands (Phase 1.8).")]
|
||||
private readonly NetworkVariable<float> racePickTimer = new NetworkVariable<float>(
|
||||
value: 0f,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
/// <summary>STUBBED. Seconds remaining in the race-pick countdown. Zero when no pick is in progress.</summary>
|
||||
public float RacePickTimer => racePickTimer.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Fired on every peer (server and clients) when the phase changes.
|
||||
/// Subscribe to react to phase transitions without polling.
|
||||
/// </summary>
|
||||
public event System.Action<MatchPhase, MatchPhase> OnPhaseChanged;
|
||||
|
||||
// --- Lifecycle ---------------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogError("[MatchState] Duplicate MatchState detected — only one may exist per scene. " +
|
||||
"Despawning the duplicate.");
|
||||
NetworkObject.Despawn();
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
phase.OnValueChanged += HandlePhaseChanged;
|
||||
|
||||
Debug.Log($"[MatchState] Spawned. Phase={phase.Value}");
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
phase.OnValueChanged -= HandlePhaseChanged;
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
// --- Server API --------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Transitions to a new phase. Server-only; no-op on clients with a log warning.
|
||||
/// </summary>
|
||||
public void SetPhase(MatchPhase next)
|
||||
{
|
||||
if (!IsServer) { Debug.LogWarning("[MatchState] SetPhase called on a client — ignored."); return; }
|
||||
if (phase.Value == next) return;
|
||||
phase.Value = next;
|
||||
}
|
||||
|
||||
/// <summary>STUBBED. Sets the current wave index. Called by the wave system (Phase 1.5/1.6).</summary>
|
||||
public void SetCurrentWave(int wave)
|
||||
{
|
||||
if (!IsServer) { Debug.LogWarning("[MatchState] SetCurrentWave called on a client — ignored."); return; }
|
||||
currentWave.Value = wave;
|
||||
}
|
||||
|
||||
/// <summary>STUBBED. Sets the shared lives pool. Called by the combat/leak system (Phase 1.4).</summary>
|
||||
public void SetLives(int value)
|
||||
{
|
||||
if (!IsServer) { Debug.LogWarning("[MatchState] SetLives called on a client — ignored."); return; }
|
||||
lives.Value = Mathf.Max(0, value);
|
||||
}
|
||||
|
||||
/// <summary>STUBBED. Sets the race-pick countdown timer. Called by the race-pick system (Phase 1.8).</summary>
|
||||
public void SetRacePickTimer(float seconds)
|
||||
{
|
||||
if (!IsServer) { Debug.LogWarning("[MatchState] SetRacePickTimer called on a client — ignored."); return; }
|
||||
racePickTimer.Value = Mathf.Max(0f, seconds);
|
||||
}
|
||||
|
||||
// --- Handlers ----------------------------------------------------
|
||||
|
||||
private void HandlePhaseChanged(MatchPhase previous, MatchPhase current)
|
||||
{
|
||||
Debug.Log($"[MatchState] Phase: {previous} → {current}");
|
||||
OnPhaseChanged?.Invoke(previous, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/Gameplay/MatchState.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/MatchState.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a34585be59d49e94b811d96be51c5088
|
||||
|
|
@ -45,7 +45,27 @@ namespace TD.Gameplay
|
|||
return;
|
||||
}
|
||||
|
||||
SpawnBuilderForOwner();
|
||||
var pms = GetComponent<PlayerMatchState>();
|
||||
if (pms == null)
|
||||
{
|
||||
Debug.LogError("[PlayerBuilderSpawner] PlayerMatchState not found on Player Prefab. " +
|
||||
"Add it as a sibling component.");
|
||||
return;
|
||||
}
|
||||
|
||||
// PlayerMatchState.OnNetworkSpawn may have already fired (component order: it first)
|
||||
// or may fire after us (component order: we first). Handle both cases.
|
||||
if (pms.Slot != PlayerSlot.None)
|
||||
SpawnBuilderForOwner(pms.Slot);
|
||||
else
|
||||
pms.SlotReady += OnOwnerSlotReady;
|
||||
}
|
||||
|
||||
private void OnOwnerSlotReady(PlayerSlot slot)
|
||||
{
|
||||
var pms = GetComponent<PlayerMatchState>();
|
||||
if (pms != null) pms.SlotReady -= OnOwnerSlotReady;
|
||||
SpawnBuilderForOwner(slot);
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
|
|
@ -58,11 +78,11 @@ namespace TD.Gameplay
|
|||
spawnedBuilder = null;
|
||||
}
|
||||
|
||||
private void SpawnBuilderForOwner()
|
||||
private void SpawnBuilderForOwner(PlayerSlot slot)
|
||||
{
|
||||
// Compute initial position: centroid of this player's zone.
|
||||
// Falls back to origin if loader/zone data isn't available.
|
||||
Vector3 spawnPos = ComputeZoneCentroid(OwnerToSlot(OwnerClientId));
|
||||
Vector3 spawnPos = ComputeZoneCentroid(slot);
|
||||
|
||||
var go = Instantiate(builderPrefab, spawnPos, Quaternion.identity);
|
||||
var netObj = go.GetComponent<NetworkObject>();
|
||||
|
|
@ -94,17 +114,6 @@ namespace TD.Gameplay
|
|||
|
||||
// ----- Helpers ----------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Stub mapping: client 0 = Player1, client 1 = Player2, etc.
|
||||
/// Replaced by MatchState's authoritative assignment when that lands.
|
||||
/// </summary>
|
||||
private static PlayerSlot OwnerToSlot(ulong clientId)
|
||||
{
|
||||
byte slotByte = (byte)(clientId + 1);
|
||||
if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
|
||||
return (PlayerSlot)slotByte;
|
||||
}
|
||||
|
||||
private static Vector3 ComputeZoneCentroid(PlayerSlot slot)
|
||||
{
|
||||
var loader = LevelLoader.Instance;
|
||||
|
|
|
|||
153
Assets/_Project/Scripts/Gameplay/PlayerMatchState.cs
Normal file
153
Assets/_Project/Scripts/Gameplay/PlayerMatchState.cs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
// Assets/_Project/Scripts/Gameplay/PlayerMatchState.cs
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-player match state. One instance is spawned per connected client (via
|
||||
/// <see cref="NetworkManager.PlayerPrefab"/>) and owned by that client.
|
||||
///
|
||||
/// Mirrors the <see cref="PlayerGoldManager"/> pattern: static registry keyed by
|
||||
/// <c>OwnerClientId</c>, server-authoritative <c>NetworkVariable</c>s, and a
|
||||
/// <c>Local</c> convenience accessor.
|
||||
///
|
||||
/// The server assigns a <see cref="PlayerSlot"/> 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.
|
||||
/// </summary>
|
||||
public class PlayerMatchState : NetworkBehaviour
|
||||
{
|
||||
// --- Static registry ---------------------------------------------
|
||||
|
||||
private static readonly Dictionary<ulong, PlayerMatchState> s_byClientId
|
||||
= new Dictionary<ulong, PlayerMatchState>();
|
||||
|
||||
// Tracks which slots are currently occupied so the server can assign the next free one.
|
||||
private static readonly HashSet<PlayerSlot> s_assignedSlots = new HashSet<PlayerSlot>();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="PlayerMatchState"/> for the given client, or null if not spawned.
|
||||
/// Safe to call on server or client.
|
||||
/// </summary>
|
||||
public static PlayerMatchState GetForClient(ulong clientId)
|
||||
{
|
||||
s_byClientId.TryGetValue(clientId, out var state);
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="PlayerSlot"/> for the given client, or <see cref="PlayerSlot.None"/>
|
||||
/// if the client has no spawned <see cref="PlayerMatchState"/>.
|
||||
/// Convenience wrapper over <see cref="GetForClient"/>.
|
||||
/// </summary>
|
||||
public static PlayerSlot SlotForClient(ulong clientId)
|
||||
=> GetForClient(clientId)?.Slot ?? PlayerSlot.None;
|
||||
|
||||
/// <summary>
|
||||
/// The local client's own state. Null on a dedicated server or before the
|
||||
/// local player has spawned.
|
||||
/// </summary>
|
||||
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<PlayerSlot> slot = new NetworkVariable<PlayerSlot>(
|
||||
value: PlayerSlot.None,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
private readonly NetworkVariable<FixedString32Bytes> displayName = new NetworkVariable<FixedString32Bytes>(
|
||||
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<RaceId> raceSelection = new NetworkVariable<RaceId>(
|
||||
value: RaceId.None,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
/// <summary>This player's assigned slot in the match. Authoritative once spawned.</summary>
|
||||
public PlayerSlot Slot => slot.Value;
|
||||
|
||||
/// <summary>Display name. Stub until a lobby/name system provides it.</summary>
|
||||
public string DisplayName => displayName.Value.ToString();
|
||||
|
||||
/// <summary>STUBBED. Race chosen by this player. <see cref="RaceId.None"/> until Phase 1.8.</summary>
|
||||
public RaceId RaceSelection => raceSelection.Value;
|
||||
|
||||
/// <summary>STUBBED. Sets this player's race selection. Called by the race-pick system (Phase 1.8).</summary>
|
||||
public void SetRaceSelection(RaceId race)
|
||||
{
|
||||
if (!IsServer) { Debug.LogWarning("[PlayerMatchState] SetRaceSelection called on a client — ignored."); return; }
|
||||
raceSelection.Value = race;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only. Fires on the server immediately after the slot is assigned in
|
||||
/// <see cref="OnNetworkSpawn"/>. Sibling components (e.g. PlayerBuilderSpawner)
|
||||
/// subscribe to defer work that requires a valid slot.
|
||||
/// </summary>
|
||||
public event System.Action<PlayerSlot> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4ce7a1bcbbd94474d9f5bc855c2394fc
|
||||
|
|
@ -400,21 +400,7 @@ namespace TD.Gameplay
|
|||
|
||||
// ----- Player slot ------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns the local player's PlayerSlot.
|
||||
/// STUB: Uses the same trivial client-ID → slot mapping as
|
||||
/// <c>TowerPlacementManager.ClientIdToPlayerSlot</c>. Will be replaced
|
||||
/// when MatchState carries the authoritative assignment.
|
||||
/// </summary>
|
||||
private static PlayerSlot GetLocalPlayerSlot()
|
||||
{
|
||||
var nm = Unity.Netcode.NetworkManager.Singleton;
|
||||
if (nm == null || !nm.IsClient) return PlayerSlot.None;
|
||||
|
||||
ulong clientId = nm.LocalClientId;
|
||||
byte slotByte = (byte)(clientId + 1);
|
||||
if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
|
||||
return (PlayerSlot)slotByte;
|
||||
}
|
||||
=> PlayerMatchState.Local?.Slot ?? PlayerSlot.None;
|
||||
}
|
||||
}
|
||||
|
|
@ -610,20 +610,8 @@ namespace TD.Gameplay
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a client ID to the PlayerSlot assigned to that client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// STUB: Currently uses a trivial mapping where client 0 = Player1, client 1 = Player2,
|
||||
/// etc. This will be replaced when MatchState / PlayerMatchState is implemented and
|
||||
/// carries the authoritative client-to-slot assignment.
|
||||
/// </remarks>
|
||||
private static PlayerSlot ClientIdToPlayerSlot(ulong clientId)
|
||||
{
|
||||
byte slotByte = (byte)(clientId + 1);
|
||||
if (slotByte < 1 || slotByte > 9) return PlayerSlot.None;
|
||||
return (PlayerSlot)slotByte;
|
||||
}
|
||||
=> PlayerMatchState.SlotForClient(clientId);
|
||||
|
||||
private void Reject(PlacementRequest req, PlacementRejectionReason reason)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue