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
+69 -22
View File
@@ -4,6 +4,7 @@ import { persist, type GameDb } from '@/lib/db/client'
import { BUILD_PART_COST, BASE_CAPACITY, BASE_LEVEL, SHIP_TYPES, expandCost, upgradeCost } from './ship-costs'
import { buildGate, effectiveCapacityCap, effectiveLevelCap } from './tech'
import { ensureSave, type InventoryItemDto } from './save'
import type { Task, TaskType } from './tasks'
export interface ShipSystemDto {
id: number
@@ -68,17 +69,69 @@ export async function spendItems(db: GameDb, saveId: number, kind: 'resource' |
}
}
async function bumpTurnAndEvent(db: GameDb, saveId: number, turn: number, type: string, message: string): Promise<void> {
await db.db.update(schema.saves).set({ turn }).where(eq(schema.saves.id, saveId))
await db.db.insert(schema.events).values({ saveId, turn, type, message, createdAt: new Date().toISOString() })
}
// Slugs of the researched techs on this save — drives the effective caps and build gates below.
async function researchedTechSlugs(db: GameDb): Promise<Set<string>> {
return new Set((await db.db.select().from(schema.tech)).filter((t) => t.researched).map((t) => t.slug))
}
// UPGRADE — level up a built system by one (capped at the effective max level, raised by techs). Cost: `level` units of any resource.
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 }
}
// SHIP_UPGRADE — level a built system by one, at whatever level it sits at resolution
// time. No cost here (deducted at queue time — Task 3 — or by upgradeSystem before
// delegating); no mission hooks (the current action has none).
export async function resolveShipUpgradeTask(db: GameDb, task: Task): Promise<void> {
await ensureSave(db)
const { systemId } = JSON.parse(task.payload) as { systemId: number }
const [save] = await db.db.select().from(schema.saves).limit(1)
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})`)
const cost = upgradeCost(target.level)
await db.db.update(schema.shipSystems).set({ level: target.level + 1 }).where(eq(schema.shipSystems.id, target.id))
await db.db.insert(schema.events).values({ saveId: save.id, turn: save.turn, type: 'upgrade', message: `Upgraded ${target.type} to level ${target.level + 1} (cost: ${cost}× resources)`, createdAt: new Date().toISOString() })
await persist(db)
}
// SHIP_EXPAND — same shape for capacity (cost: `capacity` units of any resource, deducted).
export async function resolveShipExpandTask(db: GameDb, task: Task): Promise<void> {
await ensureSave(db)
const { systemId } = JSON.parse(task.payload) as { systemId: number }
const [save] = await db.db.select().from(schema.saves).limit(1)
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})`)
const cost = expandCost(target.capacity)
await db.db.update(schema.shipSystems).set({ capacity: target.capacity + 1 }).where(eq(schema.shipSystems.id, target.id))
await db.db.insert(schema.events).values({ saveId: save.id, turn: save.turn, type: 'expand', message: `Expanded ${target.type} capacity to ${target.capacity + 1} (cost: ${cost}× resources)`, createdAt: new Date().toISOString() })
await persist(db)
}
// SHIP_BUILD — build an unbuilt type at base values. The flat parts cost is already
// deducted; some types are gated behind a researched tech (see EFFECTS in lib/game/tech.ts).
export async function resolveShipBuildTask(db: GameDb, task: Task): Promise<void> {
await ensureSave(db)
const { type } = JSON.parse(task.payload) as { type: string }
const [save] = await db.db.select().from(schema.saves).limit(1)
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`)
await db.db.update(schema.shipSystems).set({ level: BASE_LEVEL, capacity: BASE_CAPACITY }).where(eq(schema.shipSystems.id, target.id))
await db.db.insert(schema.events).values({ saveId: save.id, turn: save.turn, type: 'build', message: `Built ${target.type} at base values (cost: ${BUILD_PART_COST}× parts)`, createdAt: new Date().toISOString() })
await persist(db)
}
// 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)
const [save] = await db.db.select().from(schema.saves).limit(1)
@@ -88,16 +141,14 @@ export async function upgradeSystem(db: GameDb, systemId: number): Promise<ShipS
const maxLevel = effectiveLevelCap(target.type, await researchedTechSlugs(db))
if (target.level >= maxLevel) throw new ShipError(409, `${target.type} is at max level (${maxLevel})`)
const cost = upgradeCost(target.level)
await spendItems(db, save.id, 'resource', cost)
await spendItems(db, save.id, 'resource', upgradeCost(target.level))
const turn = save.turn + 1
await db.db.update(schema.shipSystems).set({ level: target.level + 1 }).where(eq(schema.shipSystems.id, target.id))
await bumpTurnAndEvent(db, save.id, turn, 'upgrade', `Upgraded ${target.type} to level ${target.level + 1} (cost: ${cost}× resources)`)
await persist(db)
await db.db.update(schema.saves).set({ turn }).where(eq(schema.saves.id, save.id))
await resolveShipUpgradeTask(db, syntheticTask('SHIP_UPGRADE', { systemId }))
return readShipState(db)
}
// EXPAND — raise a built system's capacity by one (capped at the effective max capacity, raised by techs). Cost: `capacity` units of any resource.
// 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)
const [save] = await db.db.select().from(schema.saves).limit(1)
@@ -107,17 +158,14 @@ export async function expandSystem(db: GameDb, systemId: number): Promise<ShipSt
const maxCapacity = effectiveCapacityCap(target.type, await researchedTechSlugs(db))
if (target.capacity >= maxCapacity) throw new ShipError(409, `${target.type} is at max capacity (${maxCapacity})`)
const cost = expandCost(target.capacity)
await spendItems(db, save.id, 'resource', cost)
await spendItems(db, save.id, 'resource', expandCost(target.capacity))
const turn = save.turn + 1
await db.db.update(schema.shipSystems).set({ capacity: target.capacity + 1 }).where(eq(schema.shipSystems.id, target.id))
await bumpTurnAndEvent(db, save.id, turn, 'expand', `Expanded ${target.type} capacity to ${target.capacity + 1} (cost: ${cost}× resources)`)
await persist(db)
await db.db.update(schema.saves).set({ turn }).where(eq(schema.saves.id, save.id))
await resolveShipExpandTask(db, syntheticTask('SHIP_EXPAND', { systemId }))
return readShipState(db)
}
// BUILD — build an unbuilt type at base values. Cost: a flat amount of parts. Some types are
// gated behind a researched tech (see EFFECTS in lib/game/tech.ts).
// 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)
const [save] = await db.db.select().from(schema.saves).limit(1)
@@ -129,8 +177,7 @@ export async function buildSystem(db: GameDb, type: string): Promise<ShipState>
await spendItems(db, save.id, 'part', BUILD_PART_COST)
const turn = save.turn + 1
await db.db.update(schema.shipSystems).set({ level: BASE_LEVEL, capacity: BASE_CAPACITY }).where(eq(schema.shipSystems.id, target.id))
await bumpTurnAndEvent(db, save.id, turn, 'build', `Built ${target.type} at base values (cost: ${BUILD_PART_COST}× parts)`)
await persist(db)
await db.db.update(schema.saves).set({ turn }).where(eq(schema.saves.id, save.id))
await resolveShipBuildTask(db, syntheticTask('SHIP_BUILD', { type }))
return readShipState(db)
}