296 lines
9.9 KiB
TypeScript
296 lines
9.9 KiB
TypeScript
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' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
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'],
|
||
},
|
||
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' },
|
||
},
|
||
},
|
||
required: ['outcome'],
|
||
},
|
||
],
|
||
},
|
||
Error: {
|
||
type: 'object',
|
||
properties: {
|
||
error: { type: 'string' },
|
||
},
|
||
required: ['error'],
|
||
},
|
||
},
|
||
},
|
||
};
|