Files
synthesis/src/run/meta.ts
Денис Шкабатур 5b7dbb4df3 phase 6: school data, run state, meta-progression, crisis system
- Add school data (Alchemist: H, O, C, Na, S, Fe starting kit)
- Add run cycle types: phases, escalation, crisis, body composition
- Implement run state management (create, advance phase, discoveries, spores)
- Implement meta-progression (codex, spore accumulation, run history)
- Implement crisis system (Chemical Plague with neutralization)
- Add IndexedDB persistence for meta state
- 42 new tests (335 total)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 15:10:05 +03:00

102 lines
2.9 KiB
TypeScript

/**
* Meta-Progression — persists between runs.
*
* Manages the Codex (permanent knowledge), spores (currency),
* unlocked schools, and run history.
*/
import type { MetaState, CodexEntry, RunSummary } from './types';
import type { RunState } from './types';
import { calculateSpores, countDiscoveries } from './state';
/** Create a fresh meta state (first time playing) */
export function createMetaState(): MetaState {
return {
spores: 0,
codex: [],
totalRuns: 0,
totalDeaths: 0,
unlockedSchools: ['alchemist'],
bestRunTime: 0,
bestRunDiscoveries: 0,
runHistory: [],
};
}
/** Apply run results to meta state after a run ends (death or completion) */
export function applyRunResults(meta: MetaState, run: RunState): void {
// 1. Calculate and add spores
const sporesEarned = calculateSpores(run);
meta.spores += sporesEarned;
// 2. Add new codex entries (skip duplicates)
const existingIds = new Set(meta.codex.map(e => `${e.type}:${e.id}`));
for (const elemId of run.discoveries.elements) {
const key = `element:${elemId}`;
if (!existingIds.has(key)) {
meta.codex.push({ id: elemId, type: 'element', discoveredOnRun: run.runId });
}
}
for (const reactionId of run.discoveries.reactions) {
const key = `reaction:${reactionId}`;
if (!existingIds.has(key)) {
meta.codex.push({ id: reactionId, type: 'reaction', discoveredOnRun: run.runId });
}
}
for (const compoundId of run.discoveries.compounds) {
const key = `compound:${compoundId}`;
if (!existingIds.has(key)) {
meta.codex.push({ id: compoundId, type: 'compound', discoveredOnRun: run.runId });
}
}
for (const creatureId of run.discoveries.creatures) {
const key = `creature:${creatureId}`;
if (!existingIds.has(key)) {
meta.codex.push({ id: creatureId, type: 'creature', discoveredOnRun: run.runId });
}
}
// 3. Update stats
meta.totalRuns += 1;
meta.totalDeaths += 1; // every run ends in death (for now)
const discoveryCount = countDiscoveries(run);
if (run.elapsed > meta.bestRunTime) {
meta.bestRunTime = run.elapsed;
}
if (discoveryCount > meta.bestRunDiscoveries) {
meta.bestRunDiscoveries = discoveryCount;
}
// 4. Add to run history
const summary: RunSummary = {
runId: run.runId,
schoolId: run.schoolId,
duration: run.elapsed,
phase: run.phase,
discoveries: discoveryCount,
sporesEarned,
crisisResolved: run.crisisResolved,
};
meta.runHistory.push(summary);
}
/** Check if a school is unlocked */
export function isSchoolUnlocked(meta: MetaState, schoolId: string): boolean {
return meta.unlockedSchools.includes(schoolId);
}
/** Get codex entries filtered by type */
export function getCodexEntries(
meta: MetaState,
type: CodexEntry['type'],
): CodexEntry[] {
return meta.codex.filter(e => e.type === type);
}
/** Get total codex entry count */
export function getCodexCount(meta: MetaState): number {
return meta.codex.length;
}