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
+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)