- 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>
102 lines
2.9 KiB
TypeScript
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;
|
|
}
|