* chore: add battleground UI task tracker (2026-03-28) * feat(#142): add [NonSerialized] battleground runtime state to CardModel * feat(#143): add LoadArtCrop and LoadCardRaw (RawImage) to ImageCache * feat(#148): add GameStateManager singleton and GameState/SeatState POCOs * feat(#138): add BattlegroundController scene orchestrator * feat(#139): add LifeTrackerWidget, CommanderDamageRow, PoisonCounter * feat(#140): add PhaseTrackerWidget and PhaseStepButton * feat(#141): add TurnOrderBar and TurnOrderPill * feat(#142): add CardZone and CardView battlefield zone components * feat(#143): add CommanderZoneWidget with art crop, cast counter, and tax display * feat(#144): add TokenModel POCO, TokenPanelController, and TokenRowView * chore: mark issues 1-8 as complete in task tracker * feat(#146): add StackZoneController, StackItemView, and ManaPoolDisplay * feat(#147 #150 #151): add LobbySetupModal, ZoneViewerModal, EliminationHandler; add Play Live button to SimulationController * feat(#151): add Play Live button to SimulationController * feat(#149): add GameWebSocketClient with reconnect, ping, and main-thread dispatch * fix: add PostGameAction to ApiClient + update TODO.md all issues complete - ApiClient.cs: add PostGameAction() wrapper → POST /api/game/action (fixes compile error in HumanActionBar.cs) - TODO.md: mark issues #145, #146, #147, #149, #150, #151 as ✅ all 14 battleground issues now committed * feat(game): add routes/game.py + wire lab_api + fix GameWebSocketClient game_id URL - routes/game.py: POST /api/game/start, POST /api/game/action (new) - lab_api.py: import + register game_router - GameWebSocketClient.cs: read game_id from PlayerPrefs, build ws://{host}/ws/game/{game_id} — fixes URL mismatch with ws_game.py * fix(lobby): wire OnConfirm to POST /api/game/start, save game_id to PlayerPrefs - OnConfirm() now calls ApiClient.PostGameStart() with 4-seat config - On success: saves BattleSeats + GameId + WsUrl to PlayerPrefs, loads Battleground scene - On failure: shows inline error label (red), re-enables Confirm button - Added StartGameRequest/Response POCOs matching routes/game.py schema - Added _errorLabel [SerializeField] for user-facing error feedback
298 lines
13 KiB
C#
298 lines
13 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
using UnityEngine.Networking;
|
|
|
|
namespace CommanderAILab.Services
|
|
{
|
|
/// <summary>
|
|
/// Singleton HTTP client for Commander AI Lab FastAPI backend.
|
|
/// All REST calls go through this service.
|
|
/// </summary>
|
|
public class ApiClient : MonoBehaviour
|
|
{
|
|
public static ApiClient Instance { get; private set; }
|
|
|
|
[SerializeField] private string baseUrl = "http://localhost:8080";
|
|
[SerializeField] private float timeoutSeconds = 30f;
|
|
|
|
public string BaseUrl
|
|
{
|
|
get => baseUrl;
|
|
set
|
|
{
|
|
baseUrl = value.TrimEnd('/');
|
|
PlayerPrefs.SetString("ApiBaseUrl", baseUrl);
|
|
}
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
|
Instance = this;
|
|
DontDestroyOnLoad(gameObject);
|
|
baseUrl = PlayerPrefs.GetString("ApiBaseUrl", baseUrl);
|
|
}
|
|
|
|
// -- Generic Helpers ------------------------------------------------
|
|
|
|
public Coroutine Get<T>(string path, Action<T> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(GetCoroutine(path, onSuccess, onError));
|
|
}
|
|
|
|
public Coroutine Post<TReq, TRes>(string path, TReq body, Action<TRes> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(PostCoroutine(path, body, onSuccess, onError));
|
|
}
|
|
|
|
public Coroutine PostRaw(string path, string json, Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(PostRawCoroutine(path, json, onSuccess, onError));
|
|
}
|
|
|
|
public Coroutine GetRaw(string path, Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(GetRawCoroutine(path, onSuccess, onError));
|
|
}
|
|
|
|
public Coroutine PatchRaw(string path, string json, Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(PatchRawCoroutine(path, json, onSuccess, onError));
|
|
}
|
|
|
|
public Coroutine Delete(string path, Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(DeleteCoroutine(path, onSuccess, onError));
|
|
}
|
|
|
|
// -- Coroutine implementations --------------------------------------
|
|
|
|
private IEnumerator GetCoroutine<T>(string path, Action<T> onSuccess, Action<string> onError)
|
|
{
|
|
var url = $"{baseUrl}{path}";
|
|
using var request = UnityWebRequest.Get(url);
|
|
request.timeout = (int)timeoutSeconds;
|
|
yield return request.SendWebRequest();
|
|
if (request.result != UnityWebRequest.Result.Success)
|
|
{
|
|
Debug.LogWarning($"[ApiClient] GET {path} failed: {request.error}");
|
|
onError?.Invoke(request.error);
|
|
yield break;
|
|
}
|
|
var obj = JsonUtility.FromJson<T>(request.downloadHandler.text);
|
|
onSuccess?.Invoke(obj);
|
|
}
|
|
|
|
private IEnumerator PostCoroutine<TReq, TRes>(string path, TReq body, Action<TRes> onSuccess, Action<string> onError)
|
|
{
|
|
var url = $"{baseUrl}{path}";
|
|
var json = JsonUtility.ToJson(body);
|
|
using var request = new UnityWebRequest(url, "POST");
|
|
request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json));
|
|
request.downloadHandler = new DownloadHandlerBuffer();
|
|
request.SetRequestHeader("Content-Type", "application/json");
|
|
request.timeout = (int)timeoutSeconds;
|
|
yield return request.SendWebRequest();
|
|
if (request.result != UnityWebRequest.Result.Success)
|
|
{
|
|
Debug.LogWarning($"[ApiClient] POST {path} failed: {request.error}");
|
|
onError?.Invoke(request.error);
|
|
yield break;
|
|
}
|
|
var obj = JsonUtility.FromJson<TRes>(request.downloadHandler.text);
|
|
onSuccess?.Invoke(obj);
|
|
}
|
|
|
|
private IEnumerator PostRawCoroutine(string path, string json, Action<string> onSuccess, Action<string> onError)
|
|
{
|
|
var url = $"{baseUrl}{path}";
|
|
using var request = new UnityWebRequest(url, "POST");
|
|
request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json));
|
|
request.downloadHandler = new DownloadHandlerBuffer();
|
|
request.SetRequestHeader("Content-Type", "application/json");
|
|
request.timeout = (int)timeoutSeconds;
|
|
yield return request.SendWebRequest();
|
|
if (request.result != UnityWebRequest.Result.Success)
|
|
{
|
|
Debug.LogWarning($"[ApiClient] POST {path} failed: {request.error}");
|
|
onError?.Invoke(request.error);
|
|
yield break;
|
|
}
|
|
onSuccess?.Invoke(request.downloadHandler.text);
|
|
}
|
|
|
|
private IEnumerator PatchRawCoroutine(string path, string json, Action<string> onSuccess, Action<string> onError)
|
|
{
|
|
var url = $"{baseUrl}{path}";
|
|
using var request = new UnityWebRequest(url, "PATCH");
|
|
request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json));
|
|
request.downloadHandler = new DownloadHandlerBuffer();
|
|
request.SetRequestHeader("Content-Type", "application/json");
|
|
request.timeout = (int)timeoutSeconds;
|
|
yield return request.SendWebRequest();
|
|
if (request.result != UnityWebRequest.Result.Success)
|
|
{
|
|
Debug.LogWarning($"[ApiClient] PATCH {path} failed: {request.error}");
|
|
onError?.Invoke(request.error);
|
|
yield break;
|
|
}
|
|
onSuccess?.Invoke(request.downloadHandler.text);
|
|
}
|
|
|
|
private IEnumerator DeleteCoroutine(string path, Action<string> onSuccess, Action<string> onError)
|
|
{
|
|
var url = $"{baseUrl}{path}";
|
|
using var request = UnityWebRequest.Delete(url);
|
|
request.downloadHandler = new DownloadHandlerBuffer();
|
|
request.timeout = (int)timeoutSeconds;
|
|
yield return request.SendWebRequest();
|
|
if (request.result != UnityWebRequest.Result.Success)
|
|
{
|
|
Debug.LogWarning($"[ApiClient] DELETE {path} failed: {request.error}");
|
|
onError?.Invoke(request.error);
|
|
yield break;
|
|
}
|
|
onSuccess?.Invoke(request.downloadHandler.text);
|
|
}
|
|
|
|
// -- Raw GET helper (returns JSON string) ---------------------------
|
|
|
|
private IEnumerator GetRawCoroutine(string path, Action<string> onSuccess, Action<string> onError)
|
|
{
|
|
var url = $"{baseUrl}{path}";
|
|
using var request = UnityWebRequest.Get(url);
|
|
request.timeout = (int)timeoutSeconds;
|
|
yield return request.SendWebRequest();
|
|
if (request.result != UnityWebRequest.Result.Success)
|
|
{
|
|
Debug.LogWarning($"[ApiClient] GET {path} failed: {request.error}");
|
|
onError?.Invoke(request.error);
|
|
yield break;
|
|
}
|
|
onSuccess?.Invoke(request.downloadHandler.text);
|
|
}
|
|
|
|
// -- Health Check ---------------------------------------------------
|
|
|
|
public Coroutine HealthCheck(Action<bool> callback)
|
|
{
|
|
return StartCoroutine(HealthCheckCoroutine(callback));
|
|
}
|
|
|
|
private IEnumerator HealthCheckCoroutine(Action<bool> callback)
|
|
{
|
|
using var request = UnityWebRequest.Get($"{baseUrl}/api/health");
|
|
request.timeout = 5;
|
|
yield return request.SendWebRequest();
|
|
callback?.Invoke(request.result == UnityWebRequest.Result.Success);
|
|
}
|
|
|
|
// -- Collection -----------------------------------------------------
|
|
|
|
public Coroutine GetCollection(Action<string> onSuccess, Action<string> onError = null, string search = null)
|
|
{
|
|
var path = string.IsNullOrEmpty(search)
|
|
? "/api/collection"
|
|
: $"/api/collection?search={UnityWebRequest.EscapeURL(search)}";
|
|
return StartCoroutine(GetRawCoroutine(path, onSuccess, onError));
|
|
}
|
|
|
|
// -- Decks ----------------------------------------------------------
|
|
|
|
public Coroutine GetDecks(Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(GetRawCoroutine("/api/lab/decks", onSuccess, onError));
|
|
}
|
|
|
|
public Coroutine CreateDeck(string deckJson, Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return PostRaw("/api/decks", deckJson, onSuccess, onError);
|
|
}
|
|
|
|
// -- Simulator ------------------------------------------------------
|
|
|
|
public Coroutine GetProfiles(Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(GetRawCoroutine("/api/lab/profiles", onSuccess, onError));
|
|
}
|
|
|
|
public Coroutine StartSim(string configJson, Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return PostRaw("/api/lab/start", configJson, onSuccess, onError);
|
|
}
|
|
|
|
public Coroutine GetSimStatus(string simId, Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(GetRawCoroutine($"/api/lab/status?batchId={simId}", onSuccess, onError));
|
|
}
|
|
|
|
public Coroutine GetSimResults(string simId, Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(GetRawCoroutine($"/api/lab/result?batchId={simId}", onSuccess, onError));
|
|
}
|
|
|
|
// -- Coach ----------------------------------------------------------
|
|
|
|
public Coroutine CoachChat(string messageJson, Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return PostRaw("/api/coach/chat", messageJson, onSuccess, onError);
|
|
}
|
|
|
|
// -- Deck Generator -------------------------------------------------
|
|
|
|
public Coroutine GenerateDeck(string requestJson, Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return PostRaw("/api/deckgen/v3", requestJson, onSuccess, onError);
|
|
}
|
|
|
|
// -- Meta -----------------------------------------------------------
|
|
|
|
public Coroutine GetCommanders(Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(GetRawCoroutine("/api/lab/meta/commanders", onSuccess, onError));
|
|
}
|
|
|
|
public Coroutine GetPrecons(Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(GetRawCoroutine("/api/lab/precons", onSuccess, onError));
|
|
}
|
|
|
|
// -- Scanner --------------------------------------------------------
|
|
|
|
public Coroutine ScanCard(byte[] imageBytes, Action<string> onSuccess, Action<string> onError = null)
|
|
{
|
|
return StartCoroutine(ScanCardCoroutine(imageBytes, onSuccess, onError));
|
|
}
|
|
|
|
private IEnumerator ScanCardCoroutine(byte[] imageBytes, Action<string> onSuccess, Action<string> onError)
|
|
{
|
|
var url = $"{baseUrl}/api/scanner/scan";
|
|
var form = new WWWForm();
|
|
form.AddBinaryData("file", imageBytes, "card.jpg", "image/jpeg");
|
|
using var request = UnityWebRequest.Post(url, form);
|
|
request.timeout = (int)timeoutSeconds;
|
|
yield return request.SendWebRequest();
|
|
if (request.result != UnityWebRequest.Result.Success)
|
|
{
|
|
Debug.LogWarning($"[ApiClient] POST /api/scanner/scan failed: {request.error}");
|
|
onError?.Invoke(request.error);
|
|
yield break;
|
|
}
|
|
onSuccess?.Invoke(request.downloadHandler.text);
|
|
}
|
|
|
|
// -- Game (Live Battleground) ---------------------------------------
|
|
|
|
/// <summary>POST /api/game/action — submit a human player action during a live game.</summary>
|
|
public Coroutine PostGameAction(string json, Action<string> onSuccess, Action<string> onError = null)
|
|
=> PostRaw("/api/game/action", json, onSuccess, onError);
|
|
|
|
/// <summary>POST /api/game/start — start a new live game with the given lobby config.</summary>
|
|
public Coroutine PostGameStart(string json, Action<string> onSuccess, Action<string> onError = null)
|
|
=> PostRaw("/api/game/start", json, onSuccess, onError);
|
|
}
|
|
}
|