diff --git a/app/api/jump/route.ts b/app/api/jump/route.ts new file mode 100644 index 0000000..68bfd59 --- /dev/null +++ b/app/api/jump/route.ts @@ -0,0 +1,27 @@ +import { getGameDb } from '@/lib/db/client' +import { JumpError, jumpToSystem } from '@/lib/game/save' + +// Jump to an adjacent solar system. Body: { "systemId": number }. Returns the fresh world state. +export async function POST(req: Request) { + let body: unknown + try { + body = await req.json() + } catch { + return Response.json({ error: 'Invalid JSON body' }, { status: 400 }) + } + + const systemId = (body as { systemId?: unknown }).systemId + if (typeof systemId !== 'number' || !Number.isInteger(systemId)) { + return Response.json({ error: 'systemId must be an integer' }, { status: 400 }) + } + + try { + const db = await getGameDb() + return Response.json(await jumpToSystem(db, systemId)) + } catch (err) { + if (err instanceof JumpError) { + return Response.json({ error: err.message }, { status: err.status }) + } + throw err + } +} diff --git a/app/api/world/route.ts b/app/api/world/route.ts new file mode 100644 index 0000000..d395736 --- /dev/null +++ b/app/api/world/route.ts @@ -0,0 +1,8 @@ +import { getGameDb } from '@/lib/db/client' +import { getWorldState } from '@/lib/game/save' + +// The full world state for the single active save; auto-initializes on first hit. +export async function GET() { + const db = await getGameDb() + return Response.json(await getWorldState(db)) +} diff --git a/lib/openapi.ts b/lib/openapi.ts index e2e9262..3c7450e 100644 --- a/lib/openapi.ts +++ b/lib/openapi.ts @@ -8,7 +8,7 @@ export const openApiSpec = { info: { title: 'SpaceWanderer API', description: - 'JSON API for SpaceWanderer — a single-player turn-based space adventure. Scaffold stage: app metadata only; game endpoints (turn actions, state snapshots) land with the first game systems.', + 'JSON API for SpaceWanderer — a single-player turn-based space adventure (one save, no accounts). World endpoints: read the generated world state and jump between adjacent solar systems.', // Sourced from package.json so it stays in sync with every version bump. version: pkg.version, }, @@ -32,6 +32,74 @@ export const openApiSpec = { }, }, }, + '/api/world': { + get: { + summary: 'World state', + description: + 'The full world of the single active save — every known solar system, jump routes and the turn counter. Auto-initializes a new game (seeded world generation) when no save exists yet.', + operationId: 'getWorld', + responses: { + 200: { + description: 'Current world state', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/WorldState' }, + }, + }, + }, + }, + }, + }, + '/api/jump': { + post: { + summary: 'Jump to an adjacent solar system', + description: + 'Validates that the target is a jump route away from the current system, sets it as current (discovering it if new and generating its neighbours), increments the turn counter and writes a jump event. Returns the fresh world state.', + operationId: 'jumpToSystem', + requestBody: { + required: true, + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/JumpRequest' }, + }, + }, + }, + responses: { + 200: { + description: 'Fresh world state after the jump', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/WorldState' }, + }, + }, + }, + 400: { + description: 'Malformed body or non-integer systemId', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Error' }, + }, + }, + }, + 404: { + description: 'Unknown system id', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Error' }, + }, + }, + }, + 409: { + description: 'Target is not adjacent to the current system (or is the current system itself)', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Error' }, + }, + }, + }, + }, + }, + }, }, components: { schemas: { @@ -42,6 +110,50 @@ export const openApiSpec = { }, required: ['version'], }, + WorldSystem: { + type: 'object', + properties: { + id: { type: 'integer' }, + name: { type: 'string' }, + x: { type: 'number', description: '0–100 chart coordinate' }, + y: { type: 'number', description: '0–100 chart coordinate' }, + discovered: { type: 'boolean' }, + current: { type: 'boolean', description: 'True for the system the ship is in now' }, + }, + required: ['id', 'name', 'x', 'y', 'discovered', 'current'], + }, + WorldState: { + type: 'object', + properties: { + systems: { type: 'array', items: { $ref: '#/components/schemas/WorldSystem' } }, + edges: { + type: 'array', + description: 'Undirected jump routes as [fromId, toId] pairs (sorted, deduped)', + items: { + type: 'array', + items: { type: 'integer' }, + minItems: 2, + maxItems: 2, + }, + }, + turn: { type: 'integer', description: 'Turn counter — one per jump' }, + }, + required: ['systems', 'edges', 'turn'], + }, + JumpRequest: { + type: 'object', + properties: { + systemId: { type: 'integer', description: 'solar_systems.id of the jump target' }, + }, + required: ['systemId'], + }, + Error: { + type: 'object', + properties: { + error: { type: 'string' }, + }, + required: ['error'], + }, }, }, };