// Client-side API helper. All requests are same-origin relative paths (they are // proxied by Vite in dev and served by Express in prod), so no origin is ever // hardcoded — this works unchanged behind the Traefik proxy. import type { GameState, Preset, PresetMeta } from '@shared/types'; import type { AddActionResponse, AddDiplomacyResponse, AdvisorResponse, ConfirmNationOpResponse, CreateGameResponse, DeleteGameResponse, GameSummary, RejectNationOpResponse, RemoveQueueItemResponse, TurnResponse, } from '@shared/protocol'; async function req(path: string, init?: RequestInit): Promise { const res = await fetch(path, { headers: { 'content-type': 'application/json' }, ...init, }); if (!res.ok) { const body = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(body.error ?? `HTTP ${res.status}`); } return res.json() as Promise; } export const api = { listPresets: () => req<{ presets: PresetMeta[] }>('/api/presets'), getPreset: (id: string) => req<{ preset: Preset }>(`/api/presets/${id}`), createGame: (presetId: string, playerNationId: string) => req('/api/games', { method: 'POST', body: JSON.stringify({ presetId, playerNationId }), }), getGame: (id: string) => req<{ game: GameState }>(`/api/games/${id}`), // Delete a saved game entirely. Fast, non-LLM. Returns { deleted: true }. deleteGame: (id: string) => req(`/api/games/${id}`, { method: 'DELETE' }), // Every saved game as a lightweight summary, newest-updated first (for the // home screen's "Saved games" section / resume affordances). listGames: () => req<{ games: GameSummary[] }>('/api/games'), // Queue a free-text player action onto the pending round. No LLM call — the // round is only committed by `jump`. Returns the updated game (with pending). addAction: (id: string, text: string) => req(`/api/games/${id}/action`, { method: 'POST', body: JSON.stringify({ text }), }), // Queue a diplomatic message to a target nation onto the pending round. addDiplomacy: (id: string, to: string, message: string) => req(`/api/games/${id}/diplomacy`, { method: 'POST', body: JSON.stringify({ to, message }), }), // Remove one queued item from the pending round, by its index in `game.pending`. // Fast, non-LLM — no turn advance. Returns the updated game. removeQueueItem: (id: string, index: number) => req(`/api/games/${id}/queue/remove`, { method: 'POST', body: JSON.stringify({ index }), }), // (v0.23, #16 P4a; v0.24, #16 P4b) Confirm a pending nation-op proposal by // its index in `game.pendingNationOps`: a rename takes effect (`edits.name` // overrides the proposed name), a split partitions the nation's territory // (`edits.playerPart` picks which part the player leads, default 0), and a // merge folds its inputs into the survivor (`edits.name` overrides the // merged name). Fast, non-LLM — no turn advance. Returns the updated game. confirmNationOp: (id: string, index: number, edits?: { name?: string; playerPart?: number }) => req(`/api/games/${id}/nation-op/confirm`, { method: 'POST', body: JSON.stringify({ index, ...(edits ? { edits } : {}) }), }), // (v0.23, #16 P4a) Reject (drop) a pending nation-op proposal by index, // without applying it. Fast, non-LLM — no turn advance. Returns the updated game. rejectNationOp: (id: string, index: number) => req(`/api/games/${id}/nation-op/reject`, { method: 'POST', body: JSON.stringify({ index }), }), // Commit the whole pending round (or an empty one) in a single LLM turn. jump: (id: string) => req(`/api/games/${id}/jump`, { method: 'POST', body: JSON.stringify({}), }), advisor: (id: string, prompt?: string) => req(`/api/games/${id}/advisor`, { method: 'POST', body: JSON.stringify({ prompt }), }), map: (id: string) => req<{ regions: Record }>( `/api/games/${id}/map`, ), };