import pkg from '@/package.json'; // 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 and jump between adjacent solar systems.', // 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/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' }, }, }, }, }, }, }, '/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: 'Explore a planet or moon', description: 'Scans one body of the save with a deterministic haul (a resource and/or a part, or nothing), marks it explored, increments the turn counter, accumulates finds into inventory and writes an explore event. Returns the fresh system state plus this scan outcome.', operationId: 'explorePlanet', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/ExploreRequest' }, }, }, }, responses: { 200: { description: 'Fresh system state plus the scan outcome', content: { 'application/json': { schema: { $ref: '#/components/schemas/ExploreResult' }, }, }, }, 400: { description: 'Malformed body or non-integer planetId', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown planet id', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'Body was already explored (each can only be scanned once)', 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. 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', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/CrewMember' } }, }, }, }, }, }, }, '/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). Costs `level` units of any resource, consumed from the oldest cargo row first, plus a 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 after the upgrade', content: { 'application/json': { schema: { $ref: '#/components/schemas/ShipState' }, }, }, }, 400: { description: 'Malformed body or non-integer systemId', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown ship system id', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'System is not built or already at max level', 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). Costs `capacity` units of any resource, consumed from the oldest cargo row first, plus a 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 after the expansion', content: { 'application/json': { schema: { $ref: '#/components/schemas/ShipState' }, }, }, }, 400: { description: 'Malformed body or non-integer systemId', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown ship system id', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'System is not built or already at max capacity', 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. Costs a flat amount of parts, consumed from the oldest cargo row first, plus a 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 after the build', content: { 'application/json': { schema: { $ref: '#/components/schemas/ShipState' }, }, }, }, 400: { description: 'Malformed body or empty type', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown ship system type', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 409: { description: 'System is already built', 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 the cost against the cargo hold (422), spends the cost from the oldest row first, marks the tech researched, increments the turn counter and writes a research event. 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 after the research', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Tech' } }, }, }, }, 400: { description: 'Malformed body or non-integer techId', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' }, }, }, }, 404: { description: 'Unknown tech id', 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)', 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). Costs a turn and writes a mission event. 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' }, }, }, }, }, }, }, }, 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 — one per jump' }, }, required: ['systems', 'edges', 'turn'], }, JumpRequest: { type: 'object', properties: { systemId: { type: 'integer', description: 'solar_systems.id of the jump target' }, }, required: ['systemId'], }, 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'], }, 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 — one per jump, exploration or ship action' }, }, 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' }, }, required: ['techId'], }, 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'], }, UpgradeRequest: { type: 'object', properties: { systemId: { type: 'integer', description: 'ship_systems.id of the built system to level up' }, }, required: ['systemId'], }, ExpandRequest: { type: 'object', properties: { systemId: { type: 'integer', description: 'ship_systems.id of the built system to expand' }, }, required: ['systemId'], }, BuildRequest: { type: 'object', properties: { type: { type: 'string', description: "Ship system type to build, e.g. WEAPONS (see ShipSystem.type)" }, }, required: ['type'], }, 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 — one per jump or exploration' }, }, required: ['systemId', 'systemName', 'planets', 'inventory', 'turn'], }, ExploreRequest: { type: 'object', properties: { planetId: { type: 'integer', description: 'planets.id of the body to scan (planet or moon)' }, }, required: ['planetId'], }, ExploreResult: { allOf: [ { $ref: '#/components/schemas/SystemState' }, { type: 'object', properties: { outcome: { type: 'array', description: 'What this scan found — may be empty (some scans 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', anyOf: [{ $ref: '#/components/schemas/CrewMember' }, { type: 'null' }], }, }, required: ['outcome', 'crewFound'], }, ], }, Error: { type: 'object', properties: { error: { type: 'string' }, }, required: ['error'], }, }, }, };