feat(game): per-type resolvers — jump/explore/ship/research effect extraction for task resolution (#44)

This commit is contained in:
2026-10-02 13:55:32 +00:00
parent d4491e082e
commit bbcd77c41b
6 changed files with 521 additions and 60 deletions
+26 -4
View File
@@ -4,6 +4,7 @@ import { persist, type GameDb } from '@/lib/db/client'
import { EFFECTS, TECHS, techStatus, type TechDto } from './tech'
import { ensureSave } from './save'
import { ShipError, spendItems } from './ship'
import type { Task, TaskType } from './tasks'
// Technology research: the SKILL TREE tab's read + action. Researching a tech costs inventory
// items (oldest row first, like ship actions) plus a turn, and its unlock effect then gates or
@@ -46,8 +47,31 @@ export async function getTechState(db: GameDb): Promise<TechDto[]> {
})
}
function syntheticTask(type: TaskType, payload: object): Task {
return { id: 0, saveId: 0, type, payload: JSON.stringify(payload), leadCrewId: 0, helpers: [], duration: 0, remaining: 0, status: 'ACTIVE', queuedAtTurn: 0 }
}
// RESEARCH — mark the payload's tech researched at the current turn. No cost here (deducted
// at queue time — Task 3 — or by researchTech before delegating); no mission hooks (the
// current action has none).
export async function resolveResearchTask(db: GameDb, task: Task): Promise<void> {
await ensureSave(db)
const { techId } = JSON.parse(task.payload) as { techId: number }
const [save] = await db.db.select().from(schema.saves).limit(1)
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)!
if (target.researched) throw new ResearchError(409, `${def.name} is already researched`)
await db.db.update(schema.tech).set({ researched: true }).where(eq(schema.tech.id, target.id))
await db.db.insert(schema.events).values({ saveId: save.id, turn: save.turn, type: 'research', message: `Researched ${def.name} (cost: ${def.costQty}× ${def.costKind === 'resource' ? 'resources' : 'parts'})`, createdAt: new Date().toISOString() })
await persist(db)
}
// Research one tech: validates prerequisites (409, naming what is missing) and the cost (422),
// spends it from the cargo hold, marks it researched, costs a turn and writes an event.
// 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[]> {
await ensureSave(db)
const [save] = await db.db.select().from(schema.saves).limit(1)
@@ -70,10 +94,8 @@ export async function researchTech(db: GameDb, techId: number): Promise<TechDto[
}
const turn = save.turn + 1
await db.db.update(schema.tech).set({ researched: true }).where(eq(schema.tech.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: 'research', message: `Researched ${def.name} (cost: ${def.costQty}× ${def.costKind === 'resource' ? 'resources' : 'parts'})`, createdAt: new Date().toISOString() })
await persist(db)
await resolveResearchTask(db, syntheticTask('RESEARCH', { techId }))
return getTechState(db)
}