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
+4 -4
View File
@@ -138,7 +138,7 @@ describe('acceptMission', () => {
await expect(acceptMission(db, b.id)).rejects.toMatchObject({ status: 409, message: /One mission at a time/ })
})
it('activates an AVAILABLE contract, costs a turn, writes an event and keeps the board topped up', async () => {
it('activates an AVAILABLE contract, spends no turn, writes an event and keeps the board topped up', async () => {
const { db } = await freshFile()
await getWorldState(db)
const [save] = await db.db.select().from(schema.saves).limit(1)
@@ -149,11 +149,11 @@ describe('acceptMission', () => {
expect(board.find((m) => m.id === a.id)!.state).toBe('ACTIVE')
expect(board.filter((m) => m.state === 'AVAILABLE').length).toBeGreaterThanOrEqual(MIN_AVAILABLE) // topped back up
const [after] = await db.db.select().from(schema.saves).limit(1)
expect(after.turn).toBe(save.turn + 1)
expect(after.turn).toBe(save.turn) // free — no turn cost
const event = (await db.db.select().from(schema.events)).find((e) => e.type === 'mission')!
expect(event.message).toContain('Accepted mission')
expect(event.turn).toBe(save.turn + 1)
expect(event.turn).toBe(save.turn) // stamped at the current turn — accept spends none
})
})
@@ -185,7 +185,7 @@ describe('completion', () => {
expect(reward.qty).toBe(2)
const event = (await db.db.select().from(schema.events)).find((e) => e.type === 'mission' && /complete/i.test(e.message))!
expect(event.turn).toBe(4) // accept (1) + three scans (2–4); completion is written by the third scan
expect(event.turn).toBe(3) // free accept (turn 0) + three scans (1–3); completion is written by the third scan
expect(event.message).toContain('Scan 3 bodies')
})
+10 -7
View File
@@ -34,8 +34,12 @@ export interface MissionSystem {
isCurrent: boolean
}
// Deterministic per (seed, turn) — same anchoring style as world expansion in save.ts. Every
// action costs exactly one turn, so the stream never repeats for two different top-ups.
// Deterministic per (seed, turn) — same anchoring style as world expansion in save.ts.
// Turn-advancing actions re-anchor the stream, so top-ups on different turns generate
// different contracts. Mission accept is free and tops up at the current turn, so it may
// share an anchor with a top-up that already ran at that turn (e.g. the new-game seed at
// turn 0) — the generated contract can then mirror an existing one: cosmetic, never a
// double reward (rewards are per row).
export function missionRng(seed: string, turn: number): () => number {
return mulberry32((Number(seed) + Math.imul(turn, 0x9e3779b1)) >>> 0)
}
@@ -152,8 +156,9 @@ export const REWARD_ITEM_NAME = 'MISSION REWARD'
const kindLabel = (kind: ItemKind): string => (kind === 'resource' ? 'RESOURCES' : 'PARTS')
// Accepts an AVAILABLE contract: it becomes THE active mission (one at a time), costs a turn like
// every other action and writes an event. Returns the fresh board.
// Accepts an AVAILABLE contract: it becomes THE active mission (one at a time). Instant and
// free (JC-19) — no turn, no cost; the event is stamped at the current turn. Returns the fresh
// board.
export async function acceptMission(db: GameDb, missionId: number): Promise<MissionDto[]> {
const [save] = await db.db.select().from(schema.saves).limit(1)
if (!save) throw new MissionError(404, 'No save yet')
@@ -164,10 +169,8 @@ export async function acceptMission(db: GameDb, missionId: number): Promise<Miss
const active = rows.find((m) => m.state === 'ACTIVE')
if (active) throw new MissionError(409, `One mission at a time — finish "${active.description}" first`)
const turn = save.turn + 1
await db.db.update(schema.missions).set({ state: 'ACTIVE' }).where(eq(schema.missions.id, target.id))
await db.db.update(schema.saves).set({ turn }).where(eq(schema.saves.id, save.id))
await db.db.insert(schema.events).values({ saveId: save.id, turn, type: 'mission', message: `Accepted mission — ${target.description}`, createdAt: new Date().toISOString() })
await db.db.insert(schema.events).values({ saveId: save.id, turn: save.turn, type: 'mission', message: `Accepted mission — ${target.description}`, createdAt: new Date().toISOString() })
await topUpMissionBoard(db) // one contract left the AVAILABLE list — keep the board topped up
await persist(db)
+17
View File
@@ -70,6 +70,23 @@ export async function resolveResearchTask(db: GameDb, task: Task): Promise<void>
await persist(db)
}
// The research pre-checks, extracted for the queueing route (5a(ii)): the same codes and
// messages as researchTech — unknown tech (404), already researched (409), missing prereqs
// (409, naming what is missing). The cost check stays out: the queueing path deducts at
// queue time (queueTask) and the research lands when the task completes on the tick.
export async function checkResearchPreconditions(db: GameDb, techId: number): Promise<void> {
await ensureSave(db)
const rows = await db.db.select().from(schema.tech)
const target = rows.find((t) => t.id === techId)
if (!target) throw new ResearchError(404, `Unknown tech ${techId}`)
const def = TECHS.find((t) => t.slug === target.slug)!
const researched = await researchedSlugs(db)
if (researched.has(def.slug)) throw new ResearchError(409, `${def.name} is already researched`)
const missing = def.prereqs.filter((p) => !researched.has(p)).map((slug) => TECHS.find((t) => t.slug === slug)?.name ?? slug)
if (missing.length > 0) throw new ResearchError(409, `${def.name} requires ${missing.join(' and ')} first`)
}
// Research one tech: validates prerequisites (409, naming what is missing) and the cost (422),
// spends it from the cargo hold, then delegates the unlock to the resolver at the new turn.
export async function researchTech(db: GameDb, techId: number): Promise<TechDto[]> {
+14
View File
@@ -531,6 +531,20 @@ export async function saveTaskRow(db: GameDb, task: Omit<Task, 'id' | 'saveId'>)
return row.id
}
// The route's DB-boundary hold write (the route→save pattern): queueTask's deducted inventory
// replaces the save's rows — array order = oldest first = id order, so spendItems' oldest-row-
// first consumption and the id-ordered reads stay consistent after the rewrite. Persists so
// the deduction survives a crash; the queueing routes call it with queueTask's result.
export async function saveInventory(db: GameDb, items: InventoryItemDto[]): Promise<void> {
await ensureSave(db)
const [save] = await db.db.select().from(schema.saves).limit(1)
await db.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.saveId, save.id))
for (const item of items) {
await db.db.insert(schema.inventoryItems).values({ saveId: save.id, kind: item.kind, name: item.name, qty: item.qty })
}
await persist(db)
}
export async function runTurn(db: GameDb): Promise<TickResult> {
await ensureSave(db)
const [save] = await db.db.select().from(schema.saves).limit(1)
+36
View File
@@ -131,6 +131,19 @@ export async function resolveShipBuildTask(db: GameDb, task: Task): Promise<void
await persist(db)
}
// The upgrade pre-checks, extracted for the queueing route (5a(ii)): the same codes and
// messages as upgradeSystem — unknown system (404), not built (409), at max level (409).
// Cost, turn and the level-up effect stay out: the queueing path deducts at queue time
// (queueTask) and the effect lands when the task completes on the tick.
export async function checkUpgradePreconditions(db: GameDb, systemId: number): Promise<void> {
await ensureSave(db)
const target = (await db.db.select().from(schema.shipSystems)).find((s) => s.id === systemId)
if (!target) throw new ShipError(404, `Unknown ship system ${systemId}`)
if (target.level <= 0) throw new ShipError(409, `${target.type} is not built`)
const maxLevel = effectiveLevelCap(target.type, await researchedTechSlugs(db))
if (target.level >= maxLevel) throw new ShipError(409, `${target.type} is at max level (${maxLevel})`)
}
// UPGRADE — level up a built system by one (capped at the effective max level, raised by techs).
export async function upgradeSystem(db: GameDb, systemId: number): Promise<ShipState> {
await ensureSave(db)
@@ -148,6 +161,17 @@ export async function upgradeSystem(db: GameDb, systemId: number): Promise<ShipS
return readShipState(db)
}
// The expand pre-checks, extracted for the queueing route (5a(ii)): the same codes and
// messages as expandSystem — unknown system (404), not built (409), at max capacity (409).
export async function checkExpandPreconditions(db: GameDb, systemId: number): Promise<void> {
await ensureSave(db)
const target = (await db.db.select().from(schema.shipSystems)).find((s) => s.id === systemId)
if (!target) throw new ShipError(404, `Unknown ship system ${systemId}`)
if (target.level <= 0) throw new ShipError(409, `${target.type} is not built`)
const maxCapacity = effectiveCapacityCap(target.type, await researchedTechSlugs(db))
if (target.capacity >= maxCapacity) throw new ShipError(409, `${target.type} is at max capacity (${maxCapacity})`)
}
// EXPAND — raise a built system's capacity by one (capped at the effective max capacity, raised by techs).
export async function expandSystem(db: GameDb, systemId: number): Promise<ShipState> {
await ensureSave(db)
@@ -165,6 +189,18 @@ export async function expandSystem(db: GameDb, systemId: number): Promise<ShipSt
return readShipState(db)
}
// The build pre-checks, extracted for the queueing route (5a(ii)): the same codes and
// messages as buildSystem — unknown type (404), already built (409), build-gate tech
// missing (409). The parts cost stays out: the queueing path deducts it at queue time.
export async function checkBuildPreconditions(db: GameDb, type: string): Promise<void> {
await ensureSave(db)
const target = (await db.db.select().from(schema.shipSystems)).find((s) => s.type === type)
if (!target) throw new ShipError(404, `Unknown ship system type ${type}`)
if (target.level > 0) throw new ShipError(409, `${target.type} is already built`)
const gate = buildGate(type, await researchedTechSlugs(db))
if (gate) throw new ShipError(409, `Building ${type} requires the ${gate} tech`)
}
// BUILD — build an unbuilt type at base values. Cost: a flat amount of parts.
export async function buildSystem(db: GameDb, type: string): Promise<ShipState> {
await ensureSave(db)
+23
View File
@@ -44,6 +44,23 @@ describe('OpenAPI spec', () => {
expect(post.responses[status].content['application/json'].schema.$ref).toBe('#/components/schemas/Error');
}
}
// The queued-ship-work requests: the action target plus the crew team (lead required,
// helpers optional) — the same team fields as JumpRequest / ExploreRequest.
const upgradeReq = openApiSpec.components.schemas.UpgradeRequest as { properties: Record<string, unknown>; required: string[] };
expect(Object.keys(upgradeReq.properties)).toEqual(['systemId', 'leadCrewId', 'helperCrewIds']);
expect(upgradeReq.required).toEqual(['systemId', 'leadCrewId']);
expect((upgradeReq.properties.helperCrewIds as { items: { type: string } }).items.type).toBe('integer');
const expandReq = openApiSpec.components.schemas.ExpandRequest as { properties: Record<string, unknown>; required: string[] };
expect(Object.keys(expandReq.properties)).toEqual(['systemId', 'leadCrewId', 'helperCrewIds']);
expect(expandReq.required).toEqual(['systemId', 'leadCrewId']);
expect((expandReq.properties.helperCrewIds as { items: { type: string } }).items.type).toBe('integer');
const buildReq = openApiSpec.components.schemas.BuildRequest as { properties: Record<string, unknown>; required: string[] };
expect(Object.keys(buildReq.properties)).toEqual(['type', 'leadCrewId', 'helperCrewIds']);
expect(buildReq.required).toEqual(['type', 'leadCrewId']);
expect((buildReq.properties.helperCrewIds as { items: { type: string } }).items.type).toBe('integer');
});
it('describes the tech endpoints: GET /api/tech → Tech[], POST /api/research with error responses', () => {
@@ -61,6 +78,12 @@ describe('OpenAPI spec', () => {
for (const status of ['400', '404', '409', '422'] as const) {
expect(post.responses[status].content['application/json'].schema.$ref).toBe('#/components/schemas/Error');
}
// The queued-research request: the tech target plus the crew team (lead required, helpers optional).
const researchReq = openApiSpec.components.schemas.ResearchRequest as { properties: Record<string, unknown>; required: string[] };
expect(Object.keys(researchReq.properties)).toEqual(['techId', 'leadCrewId', 'helperCrewIds']);
expect(researchReq.required).toEqual(['techId', 'leadCrewId']);
expect((researchReq.properties.helperCrewIds as { items: { type: string } }).items.type).toBe('integer');
});
it('describes the mission endpoints: GET /api/missions → Mission[], POST accept with error responses', () => {
+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'],
},