game: tech research action + ship-action gating by researched techs (build gate, raised caps)
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import * as schema from '@/lib/db/schema'
|
||||
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'
|
||||
|
||||
// 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
|
||||
// raises ship actions (enforced in lib/game/ship.ts via the same pure helpers).
|
||||
|
||||
// Carries an HTTP status so route handlers can map failures to responses without re-parsing.
|
||||
export class ResearchError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
message: string,
|
||||
) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
async function researchedSlugs(db: GameDb): Promise<Set<string>> {
|
||||
return new Set((await db.db.select().from(schema.tech)).filter((t) => t.researched).map((t) => t.slug))
|
||||
}
|
||||
|
||||
// The full tech tree with per-save state — RESEARCHED / AVAILABLE / LOCKED (with the missing
|
||||
// prerequisite names when locked). Auto-initializes like the other reads.
|
||||
export async function getTechState(db: GameDb): Promise<TechDto[]> {
|
||||
await ensureSave(db)
|
||||
const rows = await db.db.select().from(schema.tech)
|
||||
const researched = new Set(rows.filter((t) => t.researched).map((t) => t.slug))
|
||||
return TECHS.map((def) => {
|
||||
const row = rows.find((r) => r.slug === def.slug)! // seeded on new game / backfill v5
|
||||
const { state, missingPrereqs } = techStatus(def, researched)
|
||||
return {
|
||||
id: row.id,
|
||||
slug: def.slug,
|
||||
name: def.name,
|
||||
costKind: def.costKind,
|
||||
costQty: def.costQty,
|
||||
prereqs: def.prereqs.map((slug) => TECHS.find((t) => t.slug === slug)?.name ?? slug),
|
||||
effect: EFFECTS[def.effect]?.description ?? def.effect,
|
||||
state,
|
||||
missingPrereqs,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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.
|
||||
export async function researchTech(db: GameDb, techId: number): Promise<TechDto[]> {
|
||||
await ensureSave(db)
|
||||
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)!
|
||||
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`)
|
||||
|
||||
try {
|
||||
await spendItems(db, save.id, def.costKind, def.costQty)
|
||||
} catch (err) {
|
||||
// spendItems throws ShipError(422) on a short hold — re-throw as this domain's error.
|
||||
if (err instanceof ShipError) throw new ResearchError(err.status, err.message)
|
||||
throw err
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return getTechState(db)
|
||||
}
|
||||
Reference in New Issue
Block a user