Core engine for the Great Cycle mechanic (GDD Section IV): - GreatCycleState tracking: cycle number, run within cycle, theme - RunTrace recording: death position, school, biome, discoveries per run - Cycle advancement: auto-advance to next cycle after 7 runs - Great Renewal: resets traces, advances theme, strengthens Mycelium - 6 narrative themes (Awakening→Doubt→Realization→Attempt→Acceptance→Synthesis) - Cycle world modifiers: terrain/resources/creatures scale with cycle - Narrative data JSON with Russian/English lore fragments per theme - MetaState + persistence + RunState extended with cycle fields - 38 new tests (549 total), all passing Co-authored-by: Cursor <cursoragent@cursor.com>
371 lines
12 KiB
TypeScript
371 lines
12 KiB
TypeScript
/**
|
||
* Run Cycle Types — schools, run phases, meta-progression
|
||
*
|
||
* Defines the structure of a single run (birth → death → rebirth)
|
||
* and the persistent meta-progression between runs.
|
||
*/
|
||
|
||
// ─── Schools ─────────────────────────────────────────────────────
|
||
|
||
export interface SchoolData {
|
||
id: string;
|
||
name: string;
|
||
nameRu: string;
|
||
description: string;
|
||
descriptionRu: string;
|
||
/** Starting element symbols (looked up in element registry) */
|
||
startingElements: string[];
|
||
/** Quantity of each starting element */
|
||
startingQuantities: Record<string, number>;
|
||
/** Scientific principle the school teaches */
|
||
principle: string;
|
||
principleRu: string;
|
||
/** Play style description */
|
||
playstyle: string;
|
||
playstyleRu: string;
|
||
/** Hex color for UI representation */
|
||
color: string;
|
||
/** Passive gameplay bonuses from this school's principle */
|
||
bonuses: Record<string, number>;
|
||
/** Condition to unlock this school (absent = always unlocked) */
|
||
unlockCondition?: SchoolUnlockCondition;
|
||
}
|
||
|
||
/** Condition that must be met to unlock a school */
|
||
export interface SchoolUnlockCondition {
|
||
/** What metric to check */
|
||
type: 'elements_discovered' | 'creatures_discovered' | 'runs_completed';
|
||
/** How many needed */
|
||
threshold: number;
|
||
/** Hint text (English) shown when locked */
|
||
hint: string;
|
||
/** Hint text (Russian) shown when locked */
|
||
hintRu: string;
|
||
}
|
||
|
||
/** Resolved school bonuses with all multipliers filled in (defaults = 1.0) */
|
||
export interface ResolvedSchoolBonuses {
|
||
/** Projectile damage multiplier */
|
||
projectileDamage: number;
|
||
/** Player movement speed multiplier */
|
||
movementSpeed: number;
|
||
/** Creature aggro/flee range multiplier (< 1 = less aggro) */
|
||
creatureAggroRange: number;
|
||
/** Reaction efficiency multiplier */
|
||
reactionEfficiency: number;
|
||
}
|
||
|
||
/** Default bonuses (no school effect) */
|
||
export const DEFAULT_SCHOOL_BONUSES: ResolvedSchoolBonuses = {
|
||
projectileDamage: 1.0,
|
||
movementSpeed: 1.0,
|
||
creatureAggroRange: 1.0,
|
||
reactionEfficiency: 1.0,
|
||
};
|
||
|
||
// ─── Run Phases ──────────────────────────────────────────────────
|
||
|
||
/** Phases of a single run in order */
|
||
export enum RunPhase {
|
||
Awakening = 0,
|
||
Exploration = 1,
|
||
Escalation = 2,
|
||
Crisis = 3,
|
||
Resolution = 4,
|
||
}
|
||
|
||
/** Human-readable names for RunPhase */
|
||
export const RUN_PHASE_NAMES: Record<RunPhase, string> = {
|
||
[RunPhase.Awakening]: 'Awakening',
|
||
[RunPhase.Exploration]: 'Exploration',
|
||
[RunPhase.Escalation]: 'Escalation',
|
||
[RunPhase.Crisis]: 'Crisis',
|
||
[RunPhase.Resolution]: 'Resolution',
|
||
};
|
||
|
||
export const RUN_PHASE_NAMES_RU: Record<RunPhase, string> = {
|
||
[RunPhase.Awakening]: 'Пробуждение',
|
||
[RunPhase.Exploration]: 'Исследование',
|
||
[RunPhase.Escalation]: 'Эскалация',
|
||
[RunPhase.Crisis]: 'Кризис',
|
||
[RunPhase.Resolution]: 'Развязка',
|
||
};
|
||
|
||
// ─── Crisis Types ────────────────────────────────────────────────
|
||
|
||
export type CrisisType = 'chemical-plague';
|
||
|
||
export interface CrisisConfig {
|
||
type: CrisisType;
|
||
name: string;
|
||
nameRu: string;
|
||
description: string;
|
||
descriptionRu: string;
|
||
/** Escalation threshold (0-1) at which crisis triggers */
|
||
triggerThreshold: number;
|
||
/** Compound that neutralizes the crisis */
|
||
neutralizer: string;
|
||
/** Amount needed to neutralize */
|
||
neutralizeAmount: number;
|
||
}
|
||
|
||
// ─── Run State ───────────────────────────────────────────────────
|
||
|
||
/** State of the current run */
|
||
export interface RunState {
|
||
/** Unique run identifier */
|
||
runId: number;
|
||
/** Selected school id */
|
||
schoolId: string;
|
||
/** Biome being explored this run */
|
||
biomeId: string;
|
||
/** Current phase */
|
||
phase: RunPhase;
|
||
/** Time spent in current phase (ms) */
|
||
phaseTimer: number;
|
||
/** Total run elapsed time (ms) */
|
||
elapsed: number;
|
||
/** Escalation level 0.0 – 1.0 */
|
||
escalation: number;
|
||
/** Whether crisis has been triggered */
|
||
crisisActive: boolean;
|
||
/** Whether crisis has been resolved */
|
||
crisisResolved: boolean;
|
||
/** Discoveries made this run */
|
||
discoveries: RunDiscoveries;
|
||
/** Whether the player is alive */
|
||
alive: boolean;
|
||
/** World seed for this run */
|
||
worldSeed: number;
|
||
/** Player death position in tile coords (set on death) */
|
||
deathPosition: { tileX: number; tileY: number } | null;
|
||
}
|
||
|
||
export interface RunDiscoveries {
|
||
elements: Set<string>;
|
||
reactions: Set<string>;
|
||
compounds: Set<string>;
|
||
creatures: Set<string>;
|
||
}
|
||
|
||
// ─── Meta-Progression (persists between runs) ────────────────────
|
||
|
||
export interface CodexEntry {
|
||
id: string;
|
||
type: 'element' | 'reaction' | 'compound' | 'creature' | 'boss';
|
||
discoveredOnRun: number;
|
||
}
|
||
|
||
export interface MetaState {
|
||
/** Total spores earned across all runs */
|
||
spores: number;
|
||
/** All codex entries (permanent knowledge) */
|
||
codex: CodexEntry[];
|
||
/** Total runs completed */
|
||
totalRuns: number;
|
||
/** Total deaths */
|
||
totalDeaths: number;
|
||
/** Unlocked school ids */
|
||
unlockedSchools: string[];
|
||
/** Best run statistics */
|
||
bestRunTime: number;
|
||
bestRunDiscoveries: number;
|
||
/** Run history summaries */
|
||
runHistory: RunSummary[];
|
||
/** Mycelium knowledge graph (persistent between runs) */
|
||
mycelium: MyceliumGraphData;
|
||
/** Great cycle state (7-run macro cycles) */
|
||
greatCycle: GreatCycleState;
|
||
}
|
||
|
||
/** Serializable Mycelium graph data (stored in MetaState) */
|
||
export interface MyceliumGraphData {
|
||
/** Knowledge nodes */
|
||
nodes: MyceliumNodeData[];
|
||
/** Connections between nodes */
|
||
edges: MyceliumEdgeData[];
|
||
/** Total deposits across all runs */
|
||
totalDeposits: number;
|
||
/** Total extractions across all runs */
|
||
totalExtractions: number;
|
||
}
|
||
|
||
/** A knowledge node in the persistent Mycelium graph */
|
||
export interface MyceliumNodeData {
|
||
/** Unique node ID (format: "type:id", e.g. "element:Na") */
|
||
id: string;
|
||
/** Knowledge category */
|
||
type: 'element' | 'reaction' | 'compound' | 'creature';
|
||
/** Reference to the specific knowledge item */
|
||
knowledgeId: string;
|
||
/** Run on which this was first deposited */
|
||
depositedOnRun: number;
|
||
/** Knowledge strength 0–1 (increases with repeated deposits) */
|
||
strength: number;
|
||
}
|
||
|
||
/** An edge connecting two knowledge nodes in the Mycelium */
|
||
export interface MyceliumEdgeData {
|
||
/** Source node ID */
|
||
from: string;
|
||
/** Target node ID */
|
||
to: string;
|
||
/** Connection strength 0–1 */
|
||
weight: number;
|
||
}
|
||
|
||
export interface RunSummary {
|
||
runId: number;
|
||
schoolId: string;
|
||
duration: number;
|
||
phase: RunPhase;
|
||
discoveries: number;
|
||
sporesEarned: number;
|
||
crisisResolved: boolean;
|
||
/** Biome explored (added Phase 11) */
|
||
biomeId?: string;
|
||
/** Great cycle number when this run occurred */
|
||
cycleNumber?: number;
|
||
}
|
||
|
||
// ─── Body Composition (for death animation) ──────────────────────
|
||
|
||
/** Real elemental composition of the human body (simplified) */
|
||
export const BODY_COMPOSITION: { symbol: string; fraction: number }[] = [
|
||
{ symbol: 'O', fraction: 0.65 },
|
||
{ symbol: 'C', fraction: 0.18 },
|
||
{ symbol: 'H', fraction: 0.10 },
|
||
{ symbol: 'N', fraction: 0.03 },
|
||
{ symbol: 'Ca', fraction: 0.015 },
|
||
{ symbol: 'P', fraction: 0.01 },
|
||
{ symbol: 'S', fraction: 0.003 },
|
||
{ symbol: 'Na', fraction: 0.002 },
|
||
{ symbol: 'K', fraction: 0.002 },
|
||
{ symbol: 'Fe', fraction: 0.001 },
|
||
];
|
||
|
||
// ─── Great Cycle (every 7 runs) ──────────────────────────────────
|
||
|
||
/** Narrative themes for each great cycle (GDD Section IV) */
|
||
export type CycleTheme =
|
||
| 'awakening' // Cycle 1 (runs 1-7): learning the world
|
||
| 'doubt' // Cycle 2 (runs 8-14): finding traces of past adepts
|
||
| 'realization' // Cycle 3 (runs 15-21): understanding the nature of cycles
|
||
| 'attempt' // Cycle 4 (runs 22-28): first attempts to transcend
|
||
| 'acceptance' // Cycle 5 (runs 29-35): cycle is not a prison
|
||
| 'synthesis'; // Cycle 6+ (runs 36+): unifying all knowledge
|
||
|
||
/** Human-readable names for CycleTheme */
|
||
export const CYCLE_THEME_NAMES: Record<CycleTheme, string> = {
|
||
awakening: 'Awakening',
|
||
doubt: 'Doubt',
|
||
realization: 'Realization',
|
||
attempt: 'Attempt',
|
||
acceptance: 'Acceptance',
|
||
synthesis: 'Synthesis',
|
||
};
|
||
|
||
export const CYCLE_THEME_NAMES_RU: Record<CycleTheme, string> = {
|
||
awakening: 'Пробуждение',
|
||
doubt: 'Сомнение',
|
||
realization: 'Осознание',
|
||
attempt: 'Попытка',
|
||
acceptance: 'Принятие',
|
||
synthesis: 'Синтез',
|
||
};
|
||
|
||
/** Ordered themes by cycle number (0-indexed, cycles beyond 6 repeat 'synthesis') */
|
||
export const CYCLE_THEMES: CycleTheme[] = [
|
||
'awakening',
|
||
'doubt',
|
||
'realization',
|
||
'attempt',
|
||
'acceptance',
|
||
'synthesis',
|
||
];
|
||
|
||
/** A trace left by a completed run within a great cycle */
|
||
export interface RunTrace {
|
||
/** Run ID this trace belongs to */
|
||
runId: number;
|
||
/** Position within the great cycle (1-7) */
|
||
runInCycle: number;
|
||
/** School used this run */
|
||
schoolId: string;
|
||
/** Biome explored */
|
||
biomeId: string;
|
||
/** Death location (tile coords), null if run completed via boss victory */
|
||
deathPosition: { tileX: number; tileY: number } | null;
|
||
/** Phase reached when run ended */
|
||
phaseReached: RunPhase;
|
||
/** Was the crisis resolved? */
|
||
crisisResolved: boolean;
|
||
/** Number of unique discoveries */
|
||
discoveryCount: number;
|
||
/** Key element discoveries (up to 5 symbols for trace markers) */
|
||
keyElements: string[];
|
||
/** Duration of the run in ms */
|
||
duration: number;
|
||
/** World seed used for this run */
|
||
worldSeed: number;
|
||
}
|
||
|
||
/** Persistent great cycle state (stored in MetaState) */
|
||
export interface GreatCycleState {
|
||
/** Current great cycle number (1-based) */
|
||
cycleNumber: number;
|
||
/** Current run within the cycle (1-7, resets on renewal) */
|
||
runInCycle: number;
|
||
/** Current cycle's theme */
|
||
theme: CycleTheme;
|
||
/** Traces from runs in the CURRENT cycle (cleared on renewal) */
|
||
currentCycleTraces: RunTrace[];
|
||
/** Traces from the PREVIOUS cycle (for cross-cycle references) */
|
||
previousCycleTraces: RunTrace[];
|
||
/** Total great renewals completed */
|
||
renewalsCompleted: number;
|
||
/** Mycelium maturation level (increases each renewal) */
|
||
myceliumMaturation: number;
|
||
}
|
||
|
||
/** Number of runs per great cycle */
|
||
export const RUNS_PER_CYCLE = 7;
|
||
|
||
/** World generation modifiers based on cycle number */
|
||
export interface CycleWorldModifiers {
|
||
/** Elevation noise scale multiplier (world shape variation) */
|
||
elevationScaleMultiplier: number;
|
||
/** Detail noise scale multiplier */
|
||
detailScaleMultiplier: number;
|
||
/** Resource density multiplier */
|
||
resourceDensityMultiplier: number;
|
||
/** Creature spawn rate multiplier */
|
||
creatureSpawnMultiplier: number;
|
||
/** Escalation rate multiplier (difficulty) */
|
||
escalationRateMultiplier: number;
|
||
}
|
||
|
||
// ─── Constants ───────────────────────────────────────────────────
|
||
|
||
/** Phase durations in ms (approximate, can be adjusted) */
|
||
export const PHASE_DURATIONS: Record<RunPhase, number> = {
|
||
[RunPhase.Awakening]: 0, // ends when player leaves Cradle
|
||
[RunPhase.Exploration]: 180_000, // 3 minutes
|
||
[RunPhase.Escalation]: 120_000, // 2 minutes
|
||
[RunPhase.Crisis]: 0, // ends when resolved or player dies
|
||
[RunPhase.Resolution]: 60_000, // 1 minute
|
||
};
|
||
|
||
/** Escalation growth rate per second (0→1 over Escalation phase) */
|
||
export const ESCALATION_RATE = 0.005;
|
||
|
||
/** Spores awarded per discovery type */
|
||
export const SPORE_REWARDS = {
|
||
element: 5,
|
||
reaction: 10,
|
||
compound: 8,
|
||
creature: 15,
|
||
crisisResolved: 50,
|
||
runCompleted: 20,
|
||
};
|