From fae04a9336931c7de1e6a6e131daa06284bb1448 Mon Sep 17 00:00:00 2001 From: KoalaTrapLord <265133236+KoalaTrapLord@users.noreply.github.com> Date: Sat, 28 Mar 2026 19:54:01 -0400 Subject: [PATCH] feat(#148): add GameStateManager singleton and GameState/SeatState POCOs --- .../Assets/Scripts/Models/GameState.cs | 38 ++++++ .../UI/Battleground/GameStateManager.cs | 119 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 unity-client/Assets/Scripts/Models/GameState.cs create mode 100644 unity-client/Assets/Scripts/UI/Battleground/GameStateManager.cs diff --git a/unity-client/Assets/Scripts/Models/GameState.cs b/unity-client/Assets/Scripts/Models/GameState.cs new file mode 100644 index 0000000..18a115d --- /dev/null +++ b/unity-client/Assets/Scripts/Models/GameState.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using CommanderAILab.UI; + +namespace CommanderAILab.Models +{ + /// Full live game state snapshot pushed by the server on state_update events. + [Serializable] + public class GameState + { + [JsonProperty("seats")] public List seats; + [JsonProperty("active_seat")] public int activeSeat; + [JsonProperty("turn_number")] public int turnNumber; + [JsonProperty("phase")] public string phase; + [JsonProperty("stack")] public List stack; + } + + /// Per-seat state within a GameState snapshot. + [Serializable] + public class SeatState + { + [JsonProperty("seat_index")] public int seatIndex; + [JsonProperty("player_name")] public string playerName; + [JsonProperty("life")] public int life; + [JsonProperty("hand")] public List hand; + [JsonProperty("battlefield")] public List battlefield; + [JsonProperty("graveyard")] public List graveyard; + [JsonProperty("exile")] public List exile; + [JsonProperty("commander")] public CardModel commander; + [JsonProperty("commander_cast_count")] public int commanderCastCount; + /// Cumulative commander damage received from each seat index (length 4). + [JsonProperty("commander_damage_from")] public int[] commanderDamageFrom; + [JsonProperty("poison_counters")] public int poisonCounters; + [JsonProperty("mana_pool")] public Dictionary manaPool; + [JsonProperty("is_eliminated")] public bool isEliminated; + } +} diff --git a/unity-client/Assets/Scripts/UI/Battleground/GameStateManager.cs b/unity-client/Assets/Scripts/UI/Battleground/GameStateManager.cs new file mode 100644 index 0000000..b0af078 --- /dev/null +++ b/unity-client/Assets/Scripts/UI/Battleground/GameStateManager.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Newtonsoft.Json; +using CommanderAILab.Models; + +namespace CommanderAILab.UI +{ + /// + /// Singleton event hub for the live battleground game state. + /// All UI widgets subscribe to these events instead of polling. + /// Receives parsed server events from GameWebSocketClient. + /// + public class GameStateManager : MonoBehaviour + { + public static GameStateManager Instance { get; private set; } + + // ── Events ──────────────────────────────────────────────── + public event Action OnTurnChanged; // activeSeat + public event Action OnPhaseChanged; // phase, seat, turnNumber + public event Action OnPriorityChanged; // seatIndex + public event Action OnLifeChanged; // seatIndex, newLife + public event Action OnPlayerEliminated; // seatIndex, reason + public event Action> OnStackUpdated; + public event Action OnFullStateSync; + + // ── State ──────────────────────────────────────────────── + public int TurnNumber { get; private set; } = 1; + private GameState _state; + + // ── Lifecycle ───────────────────────────────────────────── + private void Awake() + { + if (Instance != null && Instance != this) { Destroy(gameObject); return; } + Instance = this; + } + + // ── Server event dispatch ─────────────────────────────────── + /// Called by GameWebSocketClient on the Unity main thread. + public void ApplyServerEvent(string type, string payload) + { + try + { + switch (type) + { + case "state_update": + _state = JsonConvert.DeserializeObject(payload); + if (_state != null) OnFullStateSync?.Invoke(_state); + break; + + case "turn_start": + var ts = JsonConvert.DeserializeObject(payload); + if (ts != null) { TurnNumber = ts.turnNumber; OnTurnChanged?.Invoke(ts.seat); } + break; + + case "phase_change": + var pc = JsonConvert.DeserializeObject(payload); + if (pc != null) + OnPhaseChanged?.Invoke((PhaseTrackerWidget.Phase)pc.phaseIndex, pc.seat, TurnNumber); + break; + + case "priority": + var pv = JsonConvert.DeserializeObject(payload); + if (pv != null) OnPriorityChanged?.Invoke(pv.seat); + break; + + case "life_change": + var lc = JsonConvert.DeserializeObject(payload); + if (lc != null) OnLifeChanged?.Invoke(lc.seat, lc.newLife); + break; + + case "stack_update": + var su = JsonConvert.DeserializeObject(payload); + if (su?.items != null) OnStackUpdated?.Invoke(su.items); + break; + + case "player_elim": + var pe = JsonConvert.DeserializeObject(payload); + if (pe != null) EliminatePlayer(pe.seat, pe.reason); + break; + + default: + Debug.LogWarning($"[GameStateManager] Unknown event type: {type}"); + break; + } + } + catch (Exception ex) + { + Debug.LogError($"[GameStateManager] ApplyServerEvent failed ({type}): {ex.Message}"); + } + } + + /// Trigger elimination (called by LifeTrackerWidget, CommanderDamageRow, or server event). + public void EliminatePlayer(int seatIndex, string reason) => + OnPlayerEliminated?.Invoke(seatIndex, reason); + + // ── Debug helpers (editor only) ───────────────────────────── +#if UNITY_EDITOR + private void OnGUI() + { + GUILayout.BeginArea(new Rect(10, 10, 220, 200)); + GUILayout.Label("[GSM Debug]"); + if (GUILayout.Button("Fake Turn 1 Start")) ApplyServerEvent("turn_start", "{\"seat\":0,\"turnNumber\":1}"); + if (GUILayout.Button("Advance Phase Main1")) ApplyServerEvent("phase_change","{\"phaseIndex\":3,\"seat\":0}"); + if (GUILayout.Button("Priority Seat 0")) ApplyServerEvent("priority", "{\"seat\":0}"); + if (GUILayout.Button("Elim Seat 2")) EliminatePlayer(2, "life"); + GUILayout.EndArea(); + } +#endif + + // ── Private event POCOs ─────────────────────────────────── + [Serializable] private class TurnStartEvent { public int seat; public int turnNumber; } + [Serializable] private class PhaseChangeEvent { public int phaseIndex; public int seat; } + [Serializable] private class PriorityEvent { public int seat; } + [Serializable] private class LifeChangeEvent { public int seat; public int newLife; } + [Serializable] private class StackUpdateEvent { public List items; } + [Serializable] private class EliminationEvent { public int seat; public string reason; } + } +}