import pkg from '@/package.json'; import { MAX_TEAM_SIZE } from './game/crew'; // OpenAPI 3.1 document for the app's public JSON API, kept in code so it can be // served machine-readable at /api/openapi (see AGENTS.md "## API"). Must be updated // whenever an endpoint changes. export const openApiSpec = { openapi: '3.1.0', info: { title: 'SpaceWanderer API', description: 'JSON API for SpaceWanderer — a single-player turn-based space adventure (one save, no accounts). World endpoints: read the generated world state. Jump, exploration, ship work and research queue a crew team (lead + helpers) and their effects land when the task completes on the turn tick — queueing spends no turn (jump range is adjacent by default, two hops once NAVIGATION AI is researched); mission accept is instant and free (no turn).', // Sourced from package.json so it stays in sync with every version bump. version: pkg.version, }, // Empty url = current origin (the spec is served from the same host that runs the API). servers: [{ url: '' }], paths: { '/api/version': { get: { summary: 'App version', description: 'The running app version, read from package.json.', operationId: 'getVersion', responses: { 200: { description: 'Current app version', content: { 'application/json': { schema: { $ref: '#/components/schemas/AppVersion' }, }, }, }, }, }, }, '/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/turn': { post: { summary: 'Advance exactly one turn (manual tick)', description: "Ticks the queue one turn — no request body: every ACTIVE task ages by one, tasks that reach 0 resolve (jump, scan, ship-work or research effects land, mission checks run for what resolved) and PENDING successors auto-start in FIFO order. An empty queue is a FREE SKIP — turn +1 with nothing else changed, always 200 (the UI's NEXT TURN button is never disabled; a task that fails to resolve simply does not land its effect). Returns the same world snapshot as GET /api/world, plus the task queue after the tick.", operationId: 'nextTurn', responses: { 200: { description: 'World state after the tick, plus the task queue (every task row: ACTIVE, PENDING, DONE)', content: { 'application/json': { schema: { $ref: '#/components/schemas/TurnResult' }, }, }, }, 500: { description: 'Internal error — no domain failures escape the tick (a failed task is silently dropped, not an error)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, }, '/api/jump': { post: { summary: 'Queue a jump to a solar system within range', description: 'Queues a JUMP task with the given crew team (lead + helpers): validates that the target is within jump range of the current system (one hop away, or two hops once NAVIGATION AI is researched — #35) and that the team is free, then the move — target becomes current, discovered if new with its neighbours generated, jump event — lands when the task completes on the turn tick. Queueing spends no turn. Returns the fresh world state.', operationId: 'jumpToSystem', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/JumpRequest' }, }, }, }, responses: { 200: { description: 'Fresh world state — the jump itself lands when the task completes on the turn tick', content: { 'application/json': { schema: { $ref: '#/components/schemas/WorldState' }, }, }, }, 400: { description: 'Malformed body, a non-integer systemId/leadCrewId, a non-integer-array helperCrewIds, duplicate crew ids, or a team over the size cap', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown system id or unknown crew member', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'Target is outside jump range of the current system (more than one hop away, or more than two without NAVIGATION AI; or is the current system itself) — or the crew team is blocked (a member committed to a pending task, a helper already busy, a jump in transit, or a jump already queued)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, }, '/api/system': { get: { summary: 'Current system state', description: 'The bodies of the current solar system — planets and their moons (nested via parent) with hazard/explored flags — plus the save inventory and turn counter. Auto-initializes a new game when no save exists yet.', operationId: 'getSystem', responses: { 200: { description: 'Current system state', content: { 'application/json': { schema: { $ref: '#/components/schemas/SystemState' }, }, }, }, }, }, }, '/api/explore': { post: { summary: 'Queue a scan of a planet or moon', description: 'Queues an EXPLORE task with the given crew team (lead + helpers): validates that the body exists and has not been scanned yet, and that the team is free, then the scan — a deterministic haul into the cargo hold, the explored flag, the ~18% crew roll and the explore event — lands when the task completes on the turn tick. Queueing spends no turn. Returns the fresh system state, with the outcome key empty and crewFound null until the scan resolves.', operationId: 'explorePlanet', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/ExploreRequest' }, }, }, }, responses: { 200: { description: 'Fresh system state — the scan itself lands when the task completes on the turn tick (outcome empty, crewFound null)', content: { 'application/json': { schema: { $ref: '#/components/schemas/ExploreResult' }, }, }, }, 400: { description: 'Malformed body, a non-integer planetId/leadCrewId, a non-integer-array helperCrewIds, duplicate crew ids, or a team over the size cap', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown planet id or unknown crew member', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'Body was already explored (each can only be scanned once) — or the crew team is blocked (a member committed to a pending task, a helper already busy, a jump in transit, or a jump already queued)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, }, '/api/crew': { get: { summary: 'Crew roster', description: 'The ship crew in join order — the starting crew (seeded on new game) plus members found while exploring planets/moons — returned as the { crew, tasks } object, wrapped with the active + pending task queue. Each member has one good and one bad trait and an origin. Auto-initializes a new game when no save exists yet.', operationId: 'getCrew', responses: { 200: { description: 'Crew roster plus the active + pending task queue', content: { 'application/json': { schema: { $ref: '#/components/schemas/CrewState' }, }, }, }, }, }, }, '/api/ship': { get: { summary: 'Ship state', description: "The ship's systems (built flag derived from level), the cargo hold and the turn counter. The starting ship is seeded on new game — core types built at base values, the rest unbuilt until a build action. Auto-initializes a new game when no save exists yet.", operationId: 'getShip', responses: { 200: { description: 'Current ship state', content: { 'application/json': { schema: { $ref: '#/components/schemas/ShipState' }, }, }, }, }, }, }, '/api/ship/upgrade': { post: { summary: 'Upgrade a ship system', description: "Level up a built ship system by one (capped at max level). Assign the crew team and queue the SHIP_UPGRADE task — the cost (`level` units of any resource) is deducted at queue time and the level-up lands when the task completes on the turn tick; queueing spends no turn. Returns the fresh ship state.", operationId: 'upgradeShipSystem', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/UpgradeRequest' }, }, }, }, responses: { 200: { description: 'Fresh ship state — the upgrade lands when the task completes on the turn tick', content: { 'application/json': { schema: { $ref: '#/components/schemas/ShipState' }, }, }, }, 400: { description: 'Malformed body, a non-integer systemId/leadCrewId, a non-integer-array helperCrewIds, duplicate crew ids, or a team over the size cap', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown ship system id or unknown crew member', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'System is not built or already at max level — or the crew team is blocked (a member committed to a pending task, a helper already busy, a jump in transit, or a jump already queued)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 422: { description: 'Not enough resources in the cargo hold for the cost', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, }, '/api/ship/expand': { post: { summary: 'Expand a ship system', description: "Raise a built ship system's capacity by one (capped at max capacity). Assign the crew team and queue the SHIP_EXPAND task — the cost (`capacity` units of any resource) is deducted at queue time and the capacity bump lands when the task completes on the turn tick; queueing spends no turn. Returns the fresh ship state.", operationId: 'expandShipSystem', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/ExpandRequest' }, }, }, }, responses: { 200: { description: 'Fresh ship state — the expansion lands when the task completes on the turn tick', content: { 'application/json': { schema: { $ref: '#/components/schemas/ShipState' }, }, }, }, 400: { description: 'Malformed body, a non-integer systemId/leadCrewId, a non-integer-array helperCrewIds, duplicate crew ids, or a team over the size cap', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown ship system id or unknown crew member', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'System is not built or already at max capacity — or the crew team is blocked (a member committed to a pending task, a helper already busy, a jump in transit, or a jump already queued)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 422: { description: 'Not enough resources in the cargo hold for the cost', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, }, '/api/ship/build': { post: { summary: 'Build a ship system', description: 'Build an unbuilt ship system type at base values. Assign the crew team and queue the SHIP_BUILD task — the flat parts cost is deducted at queue time and the build lands when the task completes on the turn tick; queueing spends no turn. Returns the fresh ship state.', operationId: 'buildShipSystem', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/BuildRequest' }, }, }, }, responses: { 200: { description: 'Fresh ship state — the build lands when the task completes on the turn tick', content: { 'application/json': { schema: { $ref: '#/components/schemas/ShipState' }, }, }, }, 400: { description: 'Malformed body, a non-string or empty type, a non-integer leadCrewId, a non-integer-array helperCrewIds, duplicate crew ids, or a team over the size cap', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown ship system type or unknown crew member', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'System is already built, or the build-gate tech is not researched yet — or the crew team is blocked (a member committed to a pending task, a helper already busy, a jump in transit, or a jump already queued)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 422: { description: 'Not enough parts in the cargo hold for the cost', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, }, '/api/tech': { get: { summary: 'Tech tree state', description: "The full technology tree with per-save state — RESEARCHED if done, AVAILABLE if all prerequisites are researched, LOCKED otherwise (with the missing prerequisite names). Researching a tech gates or raises ship actions (e.g. building WEAPONS requires the WEAPONS SYSTEMS tech). Auto-initializes a new game when no save exists yet.", operationId: 'getTech', responses: { 200: { description: 'The tech tree with per-save state', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Tech' } }, }, }, }, }, }, }, '/api/research': { post: { summary: 'Research a tech', description: "Validates the prerequisites (409, naming what is missing) and assigns the crew team, queueing the RESEARCH task — the tech cost is deducted at queue time and the tech is marked researched (unlock effect included) when the task completes on the turn tick; queueing spends no turn. Returns the fresh tech tree state.", operationId: 'researchTech', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/ResearchRequest' }, }, }, }, responses: { 200: { description: 'Fresh tech tree state — the research lands when the task completes on the turn tick', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Tech' } }, }, }, }, 400: { description: 'Malformed body, a non-integer techId/leadCrewId, a non-integer-array helperCrewIds, duplicate crew ids, or a team over the size cap', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown tech id or unknown crew member', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'Tech is already researched or a prerequisite is not researched yet (message names the missing prereq) — or the crew team is blocked (a member committed to a pending task, a helper already busy, a jump in transit, or a jump already queued)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 422: { description: 'Not enough of the required kind in the cargo hold for the cost', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, }, '/api/missions': { get: { summary: 'Mission board', description: "The save's mission contracts — generated deterministically from the seed, always topped up to at least three AVAILABLE. One mission is ACTIVE at a time; completion is checked after jumps and scans (DELIVER on arrival at its target system, EXPLORE after N scans, FIND when a haul contains its kind) and grants the reward into the cargo hold. Auto-initializes a new game when no save exists yet.", operationId: 'getMissions', responses: { 200: { description: 'The mission board', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Mission' } }, }, }, }, }, }, }, '/api/missions/{id}/accept': { post: { summary: 'Accept a mission', description: "Makes an AVAILABLE contract the single ACTIVE one (409 while another is active, or when it isn't AVAILABLE). Instant and free — no turn, no body — and writes a mission event at the current turn. Returns the fresh board.", operationId: 'acceptMission', parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'integer' }, description: 'missions.id of the contract to accept', }, ], responses: { 200: { description: 'Fresh mission board after the acceptance', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Mission' } }, }, }, }, 400: { description: 'Non-integer id in the path', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown mission id', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: "Mission isn't AVAILABLE, or another mission is already ACTIVE (one at a time)", content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, }, '/api/events': { get: { summary: 'Event log', description: "The save's game events, newest first — every action (new game, jump, scan, research, ship work, mission) appends one. Auto-initializes a new game when no save exists yet.", operationId: 'getEvents', parameters: [ { name: 'limit', in: 'query', required: false, schema: { type: 'integer', minimum: 1, maximum: 200 }, description: 'How many of the latest events to return (default 50)', }, ], responses: { 200: { description: 'Latest events, newest first', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Event' } }, }, }, }, 400: { description: 'limit is not an integer in [1, 200]', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, }, '/api/admin/tables': { get: { summary: 'Admin: list DB tables', description: "The game's SQLite tables exposed by the admin CRUD (#38) — name, columns (with primary-key positions) and row count. No auth by design.", operationId: 'adminListTables', responses: { 200: { description: 'Table list with column metadata and row counts', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/AdminTable' } }, }, }, }, }, }, }, '/api/admin/tables/{name}/rows': { get: { summary: 'Admin: read rows of a table', description: 'Rows of one admin table as objects keyed by column name, capped at 200 per page.', operationId: 'adminReadRows', parameters: [ { name: 'name', in: 'path', required: true, schema: { type: 'string' }, description: "Table name as listed by /api/admin/tables (e.g. 'saves')", }, { name: 'limit', in: 'query', required: false, schema: { type: 'integer', minimum: 1, maximum: 200 }, description: 'Rows per page (default 50)', }, { name: 'offset', in: 'query', required: false, schema: { type: 'integer', minimum: 0 }, description: 'Row offset for paging (default 0)', }, ], responses: { 200: { description: 'Rows plus the total row count of the table', content: { 'application/json': { schema: { $ref: '#/components/schemas/AdminRows' }, }, }, }, 400: { description: 'limit/offset out of range', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: "Unknown table name", content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, post: { summary: 'Admin: insert a row', description: "Insert one row. Body is an object of column → value (numbers for INTEGER/REAL columns, strings or null for TEXT); autoincrement ids may be omitted.", operationId: 'adminInsertRow', parameters: [ { name: 'name', in: 'path', required: true, schema: { type: 'string' }, description: "Table name as listed by /api/admin/tables", }, ], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/AdminRowValues' }, }, }, }, responses: { 200: { description: 'The inserted row', content: { 'application/json': { schema: { type: 'object' }, }, }, }, 400: { description: 'Malformed body, unknown column, missing required column or wrong value type', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: "Unknown table name", content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'Constraint violation (UNIQUE or foreign key)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, patch: { summary: 'Admin: update a row', description: "Update one row by primary key. Body is { key, values } — key holds the table's PK columns (composite keys supported), values the new column → value pairs.", operationId: 'adminUpdateRow', parameters: [ { name: 'name', in: 'path', required: true, schema: { type: 'string' }, description: "Table name as listed by /api/admin/tables", }, ], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/AdminRowUpdate' }, }, }, }, responses: { 200: { description: 'The updated row', content: { 'application/json': { schema: { type: 'object' }, }, }, }, 400: { description: 'Malformed body, bad key or unknown column', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: "Unknown table name, or no row with that key", content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'Constraint violation (UNIQUE or foreign key)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, delete: { summary: 'Admin: delete a row', description: "Delete one row by primary key. Body is { key } with the table's PK columns.", operationId: 'adminDeleteRow', parameters: [ { name: 'name', in: 'path', required: true, schema: { type: 'string' }, description: "Table name as listed by /api/admin/tables", }, ], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/AdminRowKey' }, }, }, }, responses: { 200: { description: 'The deleted row', content: { 'application/json': { schema: { type: 'object' }, }, }, }, 400: { description: 'Malformed body or bad key', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: "Unknown table name, or no row with that key", content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, }, }, }, }, components: { schemas: { AppVersion: { type: 'object', properties: { version: { type: 'string', description: 'Semver of the running build' }, }, 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 — advances on the turn tick; queued actions (jump, exploration, ship work, research) cost no turn, and mission accept is free' }, tasks: { type: 'array', description: 'The active + pending task queue — the ACTIVE block first, then the PENDING block, each ordered by (queuedAtTurn, id); DONE rows never appear', items: { $ref: '#/components/schemas/TaskDto' }, }, }, required: ['systems', 'edges', 'turn', 'tasks'], }, TurnResult: { allOf: [ { $ref: '#/components/schemas/WorldState' }, { type: 'object', properties: { tasks: { type: 'array', description: 'Every task row after the tick — ACTIVE (decremented this turn, or auto-started this turn), PENDING (waiting for its team to free) and DONE (resolved this tick or earlier)', items: { $ref: '#/components/schemas/Task' }, }, }, required: ['tasks'], }, ], }, Task: { type: 'object', properties: { id: { type: 'integer' }, saveId: { type: 'integer' }, type: { type: 'string', enum: ['JUMP', 'EXPLORE', 'SHIP_UPGRADE', 'SHIP_EXPAND', 'SHIP_BUILD', 'RESEARCH'] }, payload: { type: 'string', description: 'JSON per type: JUMP {systemId, hops} · SHIP_UPGRADE/EXPAND {systemId} · SHIP_BUILD {type} · RESEARCH {techId} · EXPLORE {planetId}', }, leadCrewId: { type: 'integer', description: 'crew.id of the team lead' }, helpers: { type: 'array', description: 'crew.id of the supporting crew (empty when none)', items: { type: 'integer' } }, duration: { type: 'integer' }, remaining: { type: 'integer' }, status: { type: 'string', enum: ['ACTIVE', 'PENDING', 'DONE'] }, queuedAtTurn: { type: 'integer' }, }, required: ['id', 'saveId', 'type', 'payload', 'leadCrewId', 'helpers', 'duration', 'remaining', 'status', 'queuedAtTurn'], }, TaskDto: { type: 'object', description: 'One row of the snapshot task queue — the ACTIVE + PENDING rows of the tasks table as the GET snapshots expose them (DONE rows never appear)', properties: { id: { type: 'integer' }, type: { type: 'string', enum: ['JUMP', 'EXPLORE', 'SHIP_UPGRADE', 'SHIP_EXPAND', 'SHIP_BUILD', 'RESEARCH'] }, targetLabel: { type: 'string', description: 'The task target by display name — the jump destination, the scanned body, the ship system with the verb suffixed (e.g. Origin — upgrade), the built system type, or the researched tech; `unknown` when the target row no longer exists', }, leadName: { type: 'string', description: 'The team lead by name; `unknown` when the crew row no longer exists' }, helperNames: { type: 'array', description: 'The supporting crew by name (empty when none)', items: { type: 'string' } }, remaining: { type: 'integer', description: 'Turns left — the stored row value (a pure read, not recomputed)' }, status: { type: 'string', enum: ['ACTIVE', 'PENDING'], description: 'The row state — the ACTIVE block precedes the PENDING block in the array', }, leadCrewId: { type: 'integer', description: 'crew.id of the team lead' }, helperCrewIds: { type: 'array', description: 'crew.id of the supporting crew (empty when none)', items: { type: 'integer' }, }, }, required: ['id', 'type', 'targetLabel', 'leadName', 'helperNames', 'remaining', 'status', 'leadCrewId', 'helperCrewIds'], }, JumpRequest: { type: 'object', properties: { systemId: { type: 'integer', description: 'solar_systems.id of the jump target' }, leadCrewId: { type: 'integer', description: 'crew.id of the team lead — the member the queued task is committed to first' }, helperCrewIds: { type: 'array', description: `Optional supporting crew (crew.id) — the whole team, lead + helpers, holds at most ${MAX_TEAM_SIZE} distinct members, all of them free`, items: { type: 'integer' }, }, }, required: ['systemId', 'leadCrewId'], }, Planet: { type: 'object', properties: { id: { type: 'integer' }, name: { type: 'string' }, type: { type: 'string', description: 'Body class, e.g. TERRESTRIAL or GAS GIANT' }, hazard: { type: 'boolean' }, explored: { type: 'boolean', description: 'True once scanned — each body can only be explored once' }, parent: { type: ['integer', 'null'], nullable: true, description: 'NULL for a planet, the parent planet id for a moon' }, }, required: ['id', 'name', 'type', 'hazard', 'explored', 'parent'], }, InventoryItem: { type: 'object', properties: { kind: { type: 'string', enum: ['resource', 'part'] }, name: { type: 'string' }, qty: { type: 'integer' }, }, required: ['kind', 'name', 'qty'], }, FoundItem: { type: 'object', properties: { kind: { type: 'string', enum: ['resource', 'part'] }, name: { type: 'string' }, qty: { type: 'integer' }, }, required: ['kind', 'name', 'qty'], }, CrewMember: { type: 'object', properties: { id: { type: 'integer' }, name: { type: 'string' }, role: { type: 'string', description: 'Ship role, e.g. PILOT or ENGINEER' }, trait_good: { type: 'string', description: 'The member good trait' }, trait_bad: { type: 'string', description: 'The member bad trait' }, origin: { type: 'string', description: "'STARTING CREW' or ', ' for members found while exploring" }, }, required: ['id', 'name', 'role', 'trait_good', 'trait_bad', 'origin'], }, CrewState: { type: 'object', description: 'GET /api/crew response — the roster wrapped with the snapshot task queue (a bare array cannot carry a sibling tasks field)', properties: { crew: { type: 'array', description: 'The ship crew in join order — the starting crew (seeded on new game) plus members found while exploring', items: { $ref: '#/components/schemas/CrewMember' }, }, tasks: { type: 'array', description: 'The active + pending task queue — the ACTIVE block first, then the PENDING block, each ordered by (queuedAtTurn, id); DONE rows never appear', items: { $ref: '#/components/schemas/TaskDto' }, }, }, required: ['crew', 'tasks'], }, ShipSystem: { type: 'object', properties: { id: { type: 'integer' }, type: { type: 'string', description: "System type, e.g. REACTOR or WEAPONS" }, level: { type: 'integer', description: '0 when not built yet' }, capacity: { type: 'integer', description: '0 when not built yet' }, built: { type: 'boolean', description: 'Derived — true when level > 0' }, }, required: ['id', 'type', 'level', 'capacity', 'built'], }, ShipState: { type: 'object', properties: { systems: { type: 'array', description: "The ship's systems in display order", items: { $ref: '#/components/schemas/ShipSystem' }, }, inventory: { type: 'array', items: { $ref: '#/components/schemas/InventoryItem' } }, turn: { type: 'integer', description: 'Turn counter — advances on the turn tick; queued actions (jump, exploration, ship work, research) cost no turn, and mission accept is free' }, }, required: ['systems', 'inventory', 'turn'], }, Tech: { type: 'object', properties: { id: { type: 'integer' }, slug: { type: 'string', description: "Stable key, e.g. WEAPONS_SYSTEMS" }, name: { type: 'string', description: "Display name, e.g. 'WEAPONS SYSTEMS'" }, costKind: { type: 'string', enum: ['resource', 'part'] }, costQty: { type: 'integer' }, prereqs: { type: 'array', description: 'Prerequisite tech names (empty for roots)', items: { type: 'string' }, }, effect: { type: 'string', description: "Human-readable unlock, e.g. 'Unlocks building the WEAPONS system'" }, state: { type: 'string', enum: ['RESEARCHED', 'AVAILABLE', 'LOCKED'] }, missingPrereqs: { type: 'array', description: 'Unmet prerequisite names — non-empty only when LOCKED', items: { type: 'string' }, }, }, required: ['id', 'slug', 'name', 'costKind', 'costQty', 'prereqs', 'effect', 'state', 'missingPrereqs'], }, ResearchRequest: { type: 'object', properties: { techId: { type: 'integer', description: 'tech.id of the tech to research' }, leadCrewId: { type: 'integer', description: 'crew.id of the team lead — the member the queued task is committed to first' }, helperCrewIds: { type: 'array', description: `Optional supporting crew (crew.id) — the whole team, lead + helpers, holds at most ${MAX_TEAM_SIZE} distinct members, all of them free`, items: { type: 'integer' }, }, }, required: ['techId', 'leadCrewId'], }, Mission: { type: 'object', properties: { id: { type: 'integer' }, type: { type: 'string', enum: ['DELIVER', 'EXPLORE', 'FIND'] }, description: { type: 'string', description: "Human-readable objective, e.g. 'Jump to Kepler Drift'" }, targetSystemId: { type: ['integer', 'null'], nullable: true, description: 'DELIVER only — destination system (NULL otherwise)' }, conditionKind: { type: ['string', 'null'], enum: ['resource', 'part', null], description: 'FIND only — item kind to haul in (NULL otherwise)' }, progressTarget: { type: 'integer', description: 'EXPLORE: scans required; 1 for the others' }, progress: { type: 'integer', description: 'Scans counted since acceptance; 0/1 for DELIVER/FIND' }, rewardKind: { type: 'string', enum: ['resource', 'part'] }, rewardQty: { type: 'integer', description: 'Reward quantity (1–3), granted into the cargo hold on completion' }, state: { type: 'string', enum: ['AVAILABLE', 'ACTIVE', 'DONE'] }, }, required: ['id', 'type', 'description', 'targetSystemId', 'conditionKind', 'progressTarget', 'progress', 'rewardKind', 'rewardQty', 'state'], }, Event: { type: 'object', properties: { id: { type: 'integer' }, turn: { type: 'integer', description: 'The save turn the event was written (new_game is 0)' }, type: { type: 'string', enum: ['new_game', 'jump', 'explore', 'research', 'upgrade', 'expand', 'build', 'mission'] }, message: { type: 'string' }, createdAt: { type: 'string', description: 'ISO 8601 timestamp' }, }, required: ['id', 'turn', 'type', 'message', 'createdAt'], }, UpgradeRequest: { type: 'object', properties: { systemId: { type: 'integer', description: 'ship_systems.id of the built system to level up' }, leadCrewId: { type: 'integer', description: 'crew.id of the team lead — the member the queued task is committed to first' }, helperCrewIds: { type: 'array', description: `Optional supporting crew (crew.id) — the whole team, lead + helpers, holds at most ${MAX_TEAM_SIZE} distinct members, all of them free`, items: { type: 'integer' }, }, }, required: ['systemId', 'leadCrewId'], }, ExpandRequest: { type: 'object', properties: { systemId: { type: 'integer', description: 'ship_systems.id of the built system to expand' }, leadCrewId: { type: 'integer', description: 'crew.id of the team lead — the member the queued task is committed to first' }, helperCrewIds: { type: 'array', description: `Optional supporting crew (crew.id) — the whole team, lead + helpers, holds at most ${MAX_TEAM_SIZE} distinct members, all of them free`, items: { type: 'integer' }, }, }, required: ['systemId', 'leadCrewId'], }, BuildRequest: { type: 'object', properties: { type: { type: 'string', description: "Ship system type to build, e.g. WEAPONS (see ShipSystem.type)" }, leadCrewId: { type: 'integer', description: 'crew.id of the team lead — the member the queued task is committed to first' }, helperCrewIds: { type: 'array', description: `Optional supporting crew (crew.id) — the whole team, lead + helpers, holds at most ${MAX_TEAM_SIZE} distinct members, all of them free`, items: { type: 'integer' }, }, }, required: ['type', 'leadCrewId'], }, SystemState: { type: 'object', properties: { systemId: { type: 'integer' }, systemName: { type: 'string' }, planets: { type: 'array', description: 'Planets and moons in display order — a moon always follows its parent row', items: { $ref: '#/components/schemas/Planet' }, }, inventory: { type: 'array', items: { $ref: '#/components/schemas/InventoryItem' } }, turn: { type: 'integer', description: 'Turn counter — advances on the turn tick; queued actions (jump, exploration, ship work, research) cost no turn, and mission accept is free' }, tasks: { type: 'array', description: 'The active + pending task queue — the ACTIVE block first, then the PENDING block, each ordered by (queuedAtTurn, id); DONE rows never appear', items: { $ref: '#/components/schemas/TaskDto' }, }, }, required: ['systemId', 'systemName', 'planets', 'inventory', 'turn', 'tasks'], }, ExploreRequest: { type: 'object', properties: { planetId: { type: 'integer', description: 'planets.id of the body to scan (planet or moon)' }, leadCrewId: { type: 'integer', description: 'crew.id of the team lead — the member the queued task is committed to first' }, helperCrewIds: { type: 'array', description: `Optional supporting crew (crew.id) — the whole team, lead + helpers, holds at most ${MAX_TEAM_SIZE} distinct members, all of them free`, items: { type: 'integer' }, }, }, required: ['planetId', 'leadCrewId'], }, ExploreResult: { allOf: [ { $ref: '#/components/schemas/SystemState' }, { type: 'object', properties: { outcome: { type: 'array', description: 'What this scan found — always empty in the queueing response; populated only once the scan resolves on the turn tick (a resolved scan may still find nothing)', items: { $ref: '#/components/schemas/FoundItem' }, }, crewFound: { description: 'A crew member who joined the ship on this scan (~18% of scans), null when none — always null in the queueing response', anyOf: [{ $ref: '#/components/schemas/CrewMember' }, { type: 'null' }], }, }, required: ['outcome', 'crewFound'], }, ], }, AdminColumn: { type: 'object', properties: { name: { type: 'string', description: "SQL column name, e.g. 'save_id'" }, type: { type: 'string', enum: ['INTEGER', 'REAL', 'TEXT'], description: 'Declared SQLite type' }, pk: { type: 'integer', description: 'Position within the primary key (0 = not a PK); composite keys come out ordered' }, }, required: ['name', 'type', 'pk'], }, AdminTable: { type: 'object', properties: { name: { type: 'string', description: "SQL table name, e.g. 'saves'" }, columns: { type: 'array', items: { $ref: '#/components/schemas/AdminColumn' } }, count: { type: 'integer', description: 'Current row count' }, }, required: ['name', 'columns', 'count'], }, AdminRows: { type: 'object', properties: { rows: { type: 'array', description: 'Rows as objects keyed by column name (null for NULL cells)', items: { type: 'object' }, }, total: { type: 'integer', description: 'Total row count of the table (before paging)' }, }, required: ['rows', 'total'], }, AdminRowValues: { type: 'object', additionalProperties: true, description: "Column → value pairs for an insert — numbers for INTEGER/REAL columns, strings or null for TEXT; autoincrement ids may be omitted", }, AdminRowKey: { type: 'object', properties: { key: { type: 'object', additionalProperties: { type: 'integer' }, description: "The table's primary-key columns (composite keys supported)", }, }, required: ['key'], }, AdminRowUpdate: { allOf: [ { $ref: '#/components/schemas/AdminRowKey' }, { type: 'object', properties: { values: { $ref: '#/components/schemas/AdminRowValues' }, }, required: ['values'], }, ], }, Error: { type: 'object', properties: { error: { type: 'string' }, }, required: ['error'], }, }, }, };