diff --git a/unity-client/Assets/Scripts/UI/Battleground/LobbySetupModal.cs b/unity-client/Assets/Scripts/UI/Battleground/LobbySetupModal.cs index 1fc0b84..790af6e 100644 --- a/unity-client/Assets/Scripts/UI/Battleground/LobbySetupModal.cs +++ b/unity-client/Assets/Scripts/UI/Battleground/LobbySetupModal.cs @@ -12,7 +12,10 @@ namespace CommanderAILab.UI /// /// Modal shown at Battleground scene start. /// Configures 4 seats (Human vs AI, deck choice, AI style). - /// On Confirm writes seat config to PlayerPrefs["BattleSeats"] and starts the game. + /// On Confirm: + /// 1. POSTs seat config to POST /api/game/start + /// 2. Saves game_id + ws_url to PlayerPrefs + /// 3. Loads Battleground scene /// public class LobbySetupModal : MonoBehaviour { @@ -23,6 +26,9 @@ namespace CommanderAILab.UI [SerializeField] private Button confirmButton; [SerializeField] private Button cancelButton; + [Header("Feedback")] + [SerializeField] private TMP_Text errorLabel; // assign in Inspector; hidden by default + [SerializeField] private ApiClient apiClient; private List _deckNames = new(); @@ -31,6 +37,8 @@ namespace CommanderAILab.UI public void Open(string savedSeatsJson) { gameObject.SetActive(true); + SetError(null); + confirmButton.onClick.AddListener(OnConfirm); cancelButton.onClick.AddListener(() => SceneManager.LoadScene("MainMenu")); @@ -64,19 +72,83 @@ namespace CommanderAILab.UI } } - // ── Confirm ───────────────────────────────────────────── + // ── Confirm → POST /api/game/start ────────────────────── private void OnConfirm() { + SetError(null); + confirmButton.interactable = false; + + // Build local seat list var configs = new List(); foreach (var row in seatRows) configs.Add(row?.GetConfig() ?? new SeatConfig()); + // Save locally so Battleground scene can read seat metadata PlayerPrefs.SetString("BattleSeats", JsonConvert.SerializeObject(configs)); PlayerPrefs.Save(); - gameObject.SetActive(false); + + // Build backend request — maps local SeatConfig → StartGameRequest + var apiSeats = new List(); + for (int i = 0; i < configs.Count; i++) + { + var c = configs[i]; + apiSeats.Add(new ApiSeatConfig + { + seat_index = i, + seat_type = c.isHuman ? "Human" : $"AI {c.aiStyle}", + deck_name = c.deckName ?? "", + player_name = c.isHuman ? "Player" : $"AI {i}", + }); + } + + var requestJson = JsonConvert.SerializeObject(new StartGameRequest { seats = apiSeats }); + + apiClient.PostGameStart( + requestJson, + onSuccess: responseJson => + { + try + { + var resp = JsonConvert.DeserializeObject(responseJson); + if (resp == null || string.IsNullOrEmpty(resp.game_id)) + { + OnStartError("Server returned an invalid response."); + return; + } + + // Persist game_id and ws_url so GameWebSocketClient can connect + PlayerPrefs.SetString("GameId", resp.game_id); + PlayerPrefs.SetString("WsUrl", resp.ws_url ?? $"/ws/game/{resp.game_id}"); + PlayerPrefs.Save(); + + gameObject.SetActive(false); + SceneManager.LoadScene("Battleground"); + } + catch (Exception ex) + { + OnStartError($"Parse error: {ex.Message}"); + } + }, + onError: err => OnStartError($"Failed to start game: {err}")); + } + + private void OnStartError(string message) + { + Debug.LogError("[LobbySetupModal] " + message); + SetError(message); + confirmButton.interactable = true; + } + + private void SetError(string message) + { + if (errorLabel == null) return; + errorLabel.text = message ?? string.Empty; + errorLabel.gameObject.SetActive(!string.IsNullOrEmpty(message)); } // ── Models ───────────────────────────────────────────── + + /// Local / PlayerPrefs representation of a seat. [Serializable] public class SeatConfig { @@ -85,6 +157,32 @@ namespace CommanderAILab.UI public string aiStyle; // Aggro / Control / Combo / Political } + /// Matches routes/game.py SeatConfig schema. + [Serializable] + private class ApiSeatConfig + { + public int seat_index; + public string seat_type; + public string deck_name; + public string player_name; + } + + /// Matches routes/game.py StartGameRequest schema. + [Serializable] + private class StartGameRequest + { + public List seats; + } + + /// Matches routes/game.py StartGameResponse schema. + [Serializable] + private class StartGameResponse + { + public string game_id; + public string status; + public string ws_url; + } + [Serializable] private class DecksWrapper { public List decks; } [Serializable] private class DeckEntry { public string name; } }