game: tech research action + ship-action gating by researched techs (build gate, raised caps)

This commit is contained in:
2026-09-29 00:24:25 +00:00
parent 4ef924edd6
commit 8818f0928d
4 changed files with 249 additions and 7 deletions
+19 -7
View File
@@ -1,7 +1,8 @@
import { eq } from 'drizzle-orm'
import * as schema from '@/lib/db/schema'
import { persist, type GameDb } from '@/lib/db/client'
import { BUILD_PART_COST, BASE_CAPACITY, BASE_LEVEL, MAX_CAPACITY, MAX_LEVEL, SHIP_TYPES, expandCost, upgradeCost } from './ship-costs'
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'
export interface ShipSystemDto {
@@ -51,7 +52,8 @@ export async function getShipState(db: GameDb): Promise<ShipState> {
// Consume `amount` units of one kind from the oldest cargo rows first (id order); rows that hit
// zero are deleted. The total is checked up front, so an insufficient hold mutates nothing.
async function spendItems(db: GameDb, saveId: number, kind: 'resource' | 'part', amount: number): Promise<void> {
// Exported: research.ts spends tech costs through the same path.
export async function spendItems(db: GameDb, saveId: number, kind: 'resource' | 'part', amount: number): Promise<void> {
const rows = (await db.db.select().from(schema.inventoryItems)).filter((i) => i.kind === kind).sort((a, b) => a.id - b.id)
if (rows.reduce((sum, r) => sum + r.qty, 0) < amount) {
throw new ShipError(422, `Insufficient ${kind === 'resource' ? 'resources' : 'parts'} in the cargo hold — need ${amount}`)
@@ -71,14 +73,20 @@ async function bumpTurnAndEvent(db: GameDb, saveId: number, turn: number, type:
await db.db.insert(schema.events).values({ saveId, turn, type, message, createdAt: new Date().toISOString() })
}
// UPGRADE — level up a built system by one (capped at MAX_LEVEL). Cost: `level` units of any resource.
// 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.
export async function upgradeSystem(db: GameDb, systemId: number): Promise<ShipState> {
await ensureSave(db)
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`)
if (target.level >= MAX_LEVEL) throw new ShipError(409, `${target.type} is at max level (${MAX_LEVEL})`)
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)
@@ -89,14 +97,15 @@ export async function upgradeSystem(db: GameDb, systemId: number): Promise<ShipS
return readShipState(db)
}
// EXPAND — raise a built system's capacity by one (capped at MAX_CAPACITY). Cost: `capacity` units of any resource.
// EXPAND — raise a built system's capacity by one (capped at the effective max capacity, raised by techs). Cost: `capacity` units of any resource.
export async function expandSystem(db: GameDb, systemId: number): Promise<ShipState> {
await ensureSave(db)
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`)
if (target.capacity >= MAX_CAPACITY) throw new ShipError(409, `${target.type} is at max capacity (${MAX_CAPACITY})`)
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)
@@ -107,13 +116,16 @@ export async function expandSystem(db: GameDb, systemId: number): Promise<ShipSt
return readShipState(db)
}
// BUILD — build an unbuilt type at base values. Cost: a flat amount of parts.
// 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).
export async function buildSystem(db: GameDb, type: string): Promise<ShipState> {
await ensureSave(db)
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 spendItems(db, save.id, 'part', BUILD_PART_COST)
const turn = save.turn + 1