Files
synthesis/src/run/types.ts
Денис Шкабатур 5f78aa1444 feat: Great Cycle system — 7-run macro cycles with traces and narrative
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>
2026-02-12 18:44:45 +03:00

371 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 01 (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 01 */
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,
};