feat(api): jump + explore endpoints queue tasks with lead + helpers instead of resolving instantly (#44)

This commit is contained in:
2026-10-02 15:47:31 +00:00
parent d7faf739a9
commit 4e312d054f
10 changed files with 455 additions and 66 deletions
+33 -20
View File
@@ -1,4 +1,5 @@
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
@@ -8,7 +9,7 @@ export const openApiSpec = {
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 solar systems within range (adjacent by default; two hops once NAVIGATION AI is researched).',
'JSON API for SpaceWanderer — a single-player turn-based space adventure (one save, no accounts). World endpoints: read the generated world state. Jump and exploration 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); ship work, research and mission accept still resolve instantly, each costing a turn.',
// Sourced from package.json so it stays in sync with every version bump.
version: pkg.version,
},
@@ -52,9 +53,9 @@ export const openApiSpec = {
},
'/api/jump': {
post: {
summary: 'Jump to a solar system within range',
summary: 'Queue a jump to a solar system within range',
description:
'Validates that the target is within jump range of the current system — one hop away, or two hops once NAVIGATION AI is researched (#35) — sets it as current (discovering it if new and generating its neighbours), advances the turn counter by the hop count and writes a jump event. Returns the fresh world state.',
'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,
@@ -66,7 +67,7 @@ export const openApiSpec = {
},
responses: {
200: {
description: 'Fresh world state after the jump',
description: 'Fresh world state — the jump itself lands when the task completes on the turn tick',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/WorldState' },
@@ -74,7 +75,7 @@ export const openApiSpec = {
},
},
400: {
description: 'Malformed body or non-integer systemId',
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' },
@@ -82,7 +83,7 @@ export const openApiSpec = {
},
},
404: {
description: 'Unknown system id',
description: 'Unknown system id or unknown crew member',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
@@ -90,7 +91,7 @@ export const openApiSpec = {
},
},
409: {
description: 'Target is outside jump range of the current system — more than one hop away, or more than two hops without NAVIGATION AI researched (or is the current system itself)',
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' },
@@ -120,9 +121,9 @@ export const openApiSpec = {
},
'/api/explore': {
post: {
summary: 'Explore a planet or moon',
summary: 'Queue a scan of 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.',
'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,
@@ -134,7 +135,7 @@ export const openApiSpec = {
},
responses: {
200: {
description: 'Fresh system state plus the scan outcome',
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' },
@@ -142,7 +143,7 @@ export const openApiSpec = {
},
},
400: {
description: 'Malformed body or non-integer planetId',
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' },
@@ -150,7 +151,7 @@ export const openApiSpec = {
},
},
404: {
description: 'Unknown planet id',
description: 'Unknown planet id or unknown crew member',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
@@ -158,7 +159,7 @@ export const openApiSpec = {
},
},
409: {
description: 'Body was already explored (each can only be scanned once)',
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' },
@@ -827,7 +828,7 @@ export const openApiSpec = {
maxItems: 2,
},
},
turn: { type: 'integer', description: 'Turn counter — one per jump' },
turn: { type: 'integer', description: 'Turn counter — advances on the turn tick and on the still-instant actions (ship work, research, mission accept); a queued jump or exploration costs no turn' },
},
required: ['systems', 'edges', 'turn'],
},
@@ -835,8 +836,14 @@ export const openApiSpec = {
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'],
required: ['systemId', 'leadCrewId'],
},
Planet: {
type: 'object',
@@ -900,7 +907,7 @@ export const openApiSpec = {
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' },
turn: { type: 'integer', description: 'Turn counter — one per ship action (research and mission accept cost one too; a queued jump or exploration costs no turn)' },
},
required: ['systems', 'inventory', 'turn'],
},
@@ -993,7 +1000,7 @@ export const openApiSpec = {
items: { $ref: '#/components/schemas/Planet' },
},
inventory: { type: 'array', items: { $ref: '#/components/schemas/InventoryItem' } },
turn: { type: 'integer', description: 'Turn counter — one per jump or exploration' },
turn: { type: 'integer', description: 'Turn counter — advances on the turn tick and on the still-instant actions (ship work, research, mission accept); a queued jump or exploration costs no turn' },
},
required: ['systemId', 'systemName', 'planets', 'inventory', 'turn'],
},
@@ -1001,8 +1008,14 @@ export const openApiSpec = {
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'],
required: ['planetId', 'leadCrewId'],
},
ExploreResult: {
allOf: [
@@ -1012,11 +1025,11 @@ export const openApiSpec = {
properties: {
outcome: {
type: 'array',
description: 'What this scan found — may be empty (some scans find nothing)',
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',
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' }],
},
},