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