Files
SpaceWanderer/lib/game/tech.ts
T
2026-09-29 14:53:11 +00:00

105 lines
5.8 KiB
TypeScript

// Pure technology model: the known techs, their costs/prerequisites and what each unlock does.
// No DB imports — both lib/game/research.ts + lib/game/ship.ts (server) and the frontend import
// from here so research/cap/gate logic can never drift from what the API enforces.
import { MAX_CAPACITY, MAX_LEVEL } from './ship-costs'
export interface TechDef {
slug: string // stable key used in DB + prerequisites
name: string // display name
costKind: 'resource' | 'part'
costQty: number
prereqs: string[] // slugs that must be researched first (short chains only)
effect: string // effect slug — see EFFECTS below
}
// The technology tree, in display order. Roots have no prerequisites; the two chains are
// WEAPONS SYSTEMS → PLASMA CANNON and REACTOR OVERCLOCK → FUSION CORE. Costs stay small so a
// handful of scans makes something affordable (the e2e scan loop relies on that).
export const TECHS: TechDef[] = [
{ slug: 'WEAPONS_SYSTEMS', name: 'WEAPONS SYSTEMS', costKind: 'part', costQty: 2, prereqs: [], effect: 'BUILD_WEAPONS' },
{ slug: 'REACTOR_OVERCLOCK', name: 'REACTOR OVERCLOCK', costKind: 'resource', costQty: 3, prereqs: [], effect: 'REACTOR_LEVEL_CAP' },
{ slug: 'HULL_PLATING', name: 'HULL PLATING', costKind: 'resource', costQty: 3, prereqs: [], effect: 'HULL_CAPACITY_CAP' },
{ slug: 'NAV_AI', name: 'NAVIGATION AI', costKind: 'resource', costQty: 2, prereqs: [], effect: 'NAV_LEVEL_CAP' },
{ slug: 'PLASMA_CANNON', name: 'PLASMA CANNON', costKind: 'part', costQty: 3, prereqs: ['WEAPONS_SYSTEMS'], effect: 'WEAPONS_LEVEL_CAP' },
{ slug: 'FUSION_CORE', name: 'FUSION CORE', costKind: 'resource', costQty: 4, prereqs: ['REACTOR_OVERCLOCK'], effect: 'REACTOR_CAPACITY_CAP' },
]
// What each researched tech changes on the ship. `unlock_build` gates a BUILD action; the cap
// effects raise the UPGRADE/EXPAND ceiling for one system type (stacked over the base caps).
export interface TechEffect {
kind: 'unlock_build' | 'raise_level_cap' | 'raise_capacity_cap'
shipType: string
bonus?: number // cap effects only
}
export const EFFECTS: Record<string, TechEffect & { description: string }> = {
BUILD_WEAPONS: { kind: 'unlock_build', shipType: 'WEAPONS', description: 'Unlocks building the WEAPONS system' },
REACTOR_LEVEL_CAP: { kind: 'raise_level_cap', shipType: 'REACTOR', bonus: 2, description: `Raises REACTOR max level by 2 (${MAX_LEVEL} → ${MAX_LEVEL + 2})` },
HULL_CAPACITY_CAP: { kind: 'raise_capacity_cap', shipType: 'HULL', bonus: 3, description: `Raises HULL max capacity by 3 (${MAX_CAPACITY} → ${MAX_CAPACITY + 3})` },
// #35 — the researched state of this tech also extends the jump range to 2 hops (enforced in
// lib/game/save.ts, which keys off the NAV_AI slug); the description is what the SKILL TREE card shows.
NAV_LEVEL_CAP: { kind: 'raise_level_cap', shipType: 'NAV COMPUTER', bonus: 2, description: `Raises NAV COMPUTER max level by 2 (${MAX_LEVEL} → ${MAX_LEVEL + 2}) and enables multi-system jumps (up to 2 hops in one action)` },
WEAPONS_LEVEL_CAP: { kind: 'raise_level_cap', shipType: 'WEAPONS', bonus: 2, description: `Raises WEAPONS max level by 2 (once built)` },
REACTOR_CAPACITY_CAP: { kind: 'raise_capacity_cap', shipType: 'REACTOR', bonus: 3, description: `Raises REACTOR max capacity by 3 (${MAX_CAPACITY} → ${MAX_CAPACITY + 3})` },
}
export type TechStateName = 'RESEARCHED' | 'AVAILABLE' | 'LOCKED'
// One tech as served to the UI (GET /api/tech) and consumed by both tabs.
export interface TechDto {
id: number
slug: string
name: string
costKind: 'resource' | 'part'
costQty: number
prereqs: string[] // prerequisite NAMES (for display), in TECHS order
effect: string // human-readable unlock description
state: TechStateName
missingPrereqs: string[] // unmet prerequisite names — non-empty only when LOCKED
}
export function techBySlug(slug: string): TechDef | undefined {
return TECHS.find((t) => t.slug === slug)
}
// RESEARCHED if done, AVAILABLE if all prerequisites are researched, LOCKED otherwise (with the
// missing prerequisite names so the UI can say which one is blocking it).
export function techStatus(def: TechDef, researchedSlugs: Set<string>): { state: TechStateName; missingPrereqs: string[] } {
if (researchedSlugs.has(def.slug)) return { state: 'RESEARCHED', missingPrereqs: [] }
const missing = def.prereqs.filter((p) => !researchedSlugs.has(p)).map((slug) => techBySlug(slug)?.name ?? slug)
return missing.length === 0 ? { state: 'AVAILABLE', missingPrereqs: [] } : { state: 'LOCKED', missingPrereqs: missing }
}
// The UPGRADE ceiling for a system type given what is researched (base cap + bonuses).
export function effectiveLevelCap(shipType: string, researchedSlugs: Set<string>): number {
let cap = MAX_LEVEL
for (const def of TECHS) {
if (!researchedSlugs.has(def.slug)) continue
const effect = EFFECTS[def.effect]
if (effect?.kind === 'raise_level_cap' && effect.shipType === shipType) cap += effect.bonus ?? 0
}
return cap
}
// The EXPAND ceiling for a system type given what is researched.
export function effectiveCapacityCap(shipType: string, researchedSlugs: Set<string>): number {
let cap = MAX_CAPACITY
for (const def of TECHS) {
if (!researchedSlugs.has(def.slug)) continue
const effect = EFFECTS[def.effect]
if (effect?.kind === 'raise_capacity_cap' && effect.shipType === shipType) cap += effect.bonus ?? 0
}
return cap
}
// The name of the tech that must be researched before a system type can be BUILT, or null when
// building it is not gated (or the gate is already researched).
export function buildGate(shipType: string, researchedSlugs: Set<string>): string | null {
for (const def of TECHS) {
const effect = EFFECTS[def.effect]
if (effect?.kind === 'unlock_build' && effect.shipType === shipType && !researchedSlugs.has(def.slug)) return def.name
}
return null
}