feat(api): GET /api/missions + POST /api/missions/{id}/accept

Routes call ensureSave first (mission functions assume an initialized save),
map MissionError to 404/409 and non-integer path ids to 400. OpenAPI spec in
code gains both paths + the Mission schema (12 -> 14 paths); openapi.test.ts
covers the new shapes.
This commit is contained in:
2026-09-29 01:55:41 +00:00
parent f356ffbe93
commit 786a5ec001
4 changed files with 138 additions and 0 deletions
+85
View File
@@ -454,6 +454,75 @@ export const openApiSpec = {
},
},
},
'/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: {
@@ -597,6 +666,22 @@ export const openApiSpec = {
},
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: {