- 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>
94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
/**
|
|
* Run State Management — creates and updates the state of a single run.
|
|
*
|
|
* A "run" is one cycle: Awakening → Exploration → Escalation → Crisis → Resolution.
|
|
* Each run tracks discoveries, escalation level, and crisis status.
|
|
*/
|
|
|
|
import {
|
|
type RunState,
|
|
type RunDiscoveries,
|
|
RunPhase,
|
|
ESCALATION_RATE,
|
|
SPORE_REWARDS,
|
|
} from './types';
|
|
|
|
/** Create a fresh run state for a new run */
|
|
export function createRunState(runId: number, schoolId: string): RunState {
|
|
return {
|
|
runId,
|
|
schoolId,
|
|
phase: RunPhase.Awakening,
|
|
phaseTimer: 0,
|
|
elapsed: 0,
|
|
escalation: 0,
|
|
crisisActive: false,
|
|
crisisResolved: false,
|
|
discoveries: {
|
|
elements: new Set<string>(),
|
|
reactions: new Set<string>(),
|
|
compounds: new Set<string>(),
|
|
creatures: new Set<string>(),
|
|
},
|
|
alive: true,
|
|
};
|
|
}
|
|
|
|
/** Advance to the next phase. Does nothing if already at Resolution. */
|
|
export function advancePhase(state: RunState): void {
|
|
if (state.phase < RunPhase.Resolution) {
|
|
state.phase = (state.phase + 1) as RunPhase;
|
|
state.phaseTimer = 0;
|
|
}
|
|
}
|
|
|
|
/** Update escalation level based on elapsed delta (ms). Only active during Escalation and Crisis. */
|
|
export function updateEscalation(state: RunState, deltaMs: number): void {
|
|
if (state.phase < RunPhase.Escalation) return;
|
|
if (state.phase > RunPhase.Crisis) return;
|
|
|
|
const deltaSec = deltaMs / 1000;
|
|
state.escalation = Math.min(1.0, state.escalation + ESCALATION_RATE * deltaSec);
|
|
}
|
|
|
|
/** Discovery type names used in API (singular) */
|
|
export type DiscoveryType = 'element' | 'reaction' | 'compound' | 'creature';
|
|
|
|
/** Map singular type to plural key in RunDiscoveries */
|
|
const DISCOVERY_KEY: Record<DiscoveryType, keyof RunDiscoveries> = {
|
|
element: 'elements',
|
|
reaction: 'reactions',
|
|
compound: 'compounds',
|
|
creature: 'creatures',
|
|
};
|
|
|
|
/** Record a discovery during the current run */
|
|
export function recordDiscovery(
|
|
state: RunState,
|
|
type: DiscoveryType,
|
|
id: string,
|
|
): void {
|
|
state.discoveries[DISCOVERY_KEY[type]].add(id);
|
|
}
|
|
|
|
/** Count total unique discoveries in a run */
|
|
export function countDiscoveries(state: RunState): number {
|
|
return (
|
|
state.discoveries.elements.size +
|
|
state.discoveries.reactions.size +
|
|
state.discoveries.compounds.size +
|
|
state.discoveries.creatures.size
|
|
);
|
|
}
|
|
|
|
/** Calculate spores earned from a run */
|
|
export function calculateSpores(state: RunState): number {
|
|
let total = 0;
|
|
total += state.discoveries.elements.size * SPORE_REWARDS.element;
|
|
total += state.discoveries.reactions.size * SPORE_REWARDS.reaction;
|
|
total += state.discoveries.compounds.size * SPORE_REWARDS.compound;
|
|
total += state.discoveries.creatures.size * SPORE_REWARDS.creature;
|
|
if (state.crisisResolved) total += SPORE_REWARDS.crisisResolved;
|
|
return total;
|
|
}
|