// Battle-report fetcher used by the Battle Viewer page. // // Phase 27 ships the BattleViewer as a logically isolated component // that accepts a `BattleReport` matching `pkg/model/report/battle.go`. // This module owns the type mirror and a single `fetchBattle` entry // point. In synthetic mode (development & e2e fixtures), the loader // falls back to a local fixture so the UI tests don't depend on a // running engine; otherwise it issues a real `GET` against the // backend gateway route added in Phase 27 step 3. import { isSyntheticGameId } from "./synthetic-report"; import { lookupSyntheticBattle } from "./synthetic-battle"; /** * BattleReport is the wire shape returned by the engine endpoint * `GET /api/v1/battle/:turn/:uuid` and forwarded by the backend * gateway as `GET /api/v1/user/games/{game_id}/battles/{turn}/{battle_id}`. * Fields mirror `pkg/model/report/battle.go`. */ export interface BattleReport { id: string; planet: number; planetName: string; races: Record; ships: Record; protocol: BattleActionReport[]; } export interface BattleReportGroup { race: string; className: string; tech: Record; num: number; numLeft: number; loadType: string; loadQuantity: number; inBattle: boolean; } export interface BattleActionReport { a: number; sa: number; d: number; sd: number; x: boolean; } export class BattleFetchError extends Error { constructor(public readonly status: number, message: string) { super(message); this.name = "BattleFetchError"; } } /** * fetchBattle returns the `BattleReport` for the supplied game, turn, * and battle id. In synthetic-report mode (DEV / e2e) the lookup is * served from `synthetic-battle.ts`; otherwise the function calls the * backend gateway route. Throws `BattleFetchError` with the upstream * status on validation or transport failure. */ export async function fetchBattle( gameId: string, turn: number, battleId: string, ): Promise { if (isSyntheticGameId(gameId)) { const fixture = lookupSyntheticBattle(battleId); if (fixture === null) { throw new BattleFetchError(404, "battle not found"); } return fixture; } const path = `/api/v1/user/games/${encodeURIComponent(gameId)}/battles/${turn}/${encodeURIComponent(battleId)}`; const response = await fetch(path, { headers: { Accept: "application/json" }, }); if (response.status === 404) { throw new BattleFetchError(404, "battle not found"); } if (!response.ok) { throw new BattleFetchError( response.status, `battle fetch failed: ${response.status}`, ); } return (await response.json()) as BattleReport; }