feat(api): ship + research endpoints queue tasks with lead + helpers; mission accept is instant and free (#44)
CI / build + unit + e2e (pull_request) Failing after 3m58s

This commit is contained in:
2026-10-02 18:36:14 +00:00
parent 4e312d054f
commit 03b8b661f6
20 changed files with 772 additions and 103 deletions
+54 -30
View File
@@ -9,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. 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.',
'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,
},
@@ -209,7 +209,7 @@ export const openApiSpec = {
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.",
"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,
@@ -221,7 +221,7 @@ export const openApiSpec = {
},
responses: {
200: {
description: 'Fresh ship state after the upgrade',
description: 'Fresh ship state — the upgrade lands when the task completes on the turn tick',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ShipState' },
@@ -229,7 +229,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' },
@@ -237,7 +237,7 @@ export const openApiSpec = {
},
},
404: {
description: 'Unknown ship system id',
description: 'Unknown ship system id or unknown crew member',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
@@ -245,7 +245,7 @@ export const openApiSpec = {
},
},
409: {
description: 'System is not built or already at max level',
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' },
@@ -267,7 +267,7 @@ export const openApiSpec = {
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.",
"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,
@@ -279,7 +279,7 @@ export const openApiSpec = {
},
responses: {
200: {
description: 'Fresh ship state after the expansion',
description: 'Fresh ship state — the expansion lands when the task completes on the turn tick',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ShipState' },
@@ -287,7 +287,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' },
@@ -295,7 +295,7 @@ export const openApiSpec = {
},
},
404: {
description: 'Unknown ship system id',
description: 'Unknown ship system id or unknown crew member',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
@@ -303,7 +303,7 @@ export const openApiSpec = {
},
},
409: {
description: 'System is not built or already at max capacity',
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' },
@@ -325,7 +325,7 @@ export const openApiSpec = {
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.',
'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,
@@ -337,7 +337,7 @@ export const openApiSpec = {
},
responses: {
200: {
description: 'Fresh ship state after the build',
description: 'Fresh ship state — the build lands when the task completes on the turn tick',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ShipState' },
@@ -345,7 +345,7 @@ export const openApiSpec = {
},
},
400: {
description: 'Malformed body or empty type',
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' },
@@ -353,7 +353,7 @@ export const openApiSpec = {
},
},
404: {
description: 'Unknown ship system type',
description: 'Unknown ship system type or unknown crew member',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
@@ -361,7 +361,7 @@ export const openApiSpec = {
},
},
409: {
description: 'System is already built',
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' },
@@ -401,7 +401,7 @@ export const openApiSpec = {
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.",
"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,
@@ -413,7 +413,7 @@ export const openApiSpec = {
},
responses: {
200: {
description: 'Fresh tech tree state after the research',
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' } },
@@ -421,7 +421,7 @@ export const openApiSpec = {
},
},
400: {
description: 'Malformed body or non-integer techId',
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' },
@@ -429,7 +429,7 @@ export const openApiSpec = {
},
},
404: {
description: 'Unknown tech id',
description: 'Unknown tech id or unknown crew member',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
@@ -437,7 +437,7 @@ export const openApiSpec = {
},
},
409: {
description: 'Tech is already researched or a prerequisite is not researched yet (message names the missing prereq)',
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' },
@@ -477,7 +477,7 @@ export const openApiSpec = {
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.",
"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: [
{
@@ -828,7 +828,7 @@ export const openApiSpec = {
maxItems: 2,
},
},
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' },
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', 'edges', 'turn'],
},
@@ -907,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 ship action (research and mission accept cost one too; a queued jump or exploration costs no turn)' },
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'],
},
@@ -938,8 +938,14 @@ export const openApiSpec = {
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'],
required: ['techId', 'leadCrewId'],
},
Mission: {
type: 'object',
@@ -961,7 +967,7 @@ export const openApiSpec = {
type: 'object',
properties: {
id: { type: 'integer' },
turn: { type: 'integer', description: 'The save turn the action cost (new_game is 0)' },
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' },
@@ -972,22 +978,40 @@ export const openApiSpec = {
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'],
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'],
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'],
required: ['type', 'leadCrewId'],
},
SystemState: {
type: 'object',
@@ -1000,7 +1024,7 @@ export const openApiSpec = {
items: { $ref: '#/components/schemas/Planet' },
},
inventory: { type: 'array', items: { $ref: '#/components/schemas/InventoryItem' } },
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' },
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: ['systemId', 'systemName', 'planets', 'inventory', 'turn'],
},