PURPOSE
Manages the lantern-activation mechanic and per-run illumination state. The player stands inside a hub’s activation circle to fill a meter; once full, the hub becomes permanently lit for the run, and its connected spokes count as lit. Illumination is the core progression metric of every run — it gates spawn pools, event variations, and VFX cascades. Once lit, always lit.
OWNS
LanternStateinterface: per-hub fill 0-1,playerInRangeflag,activatedboolean,activatedAttimestamp,hubId.IlluminationSysteminterface:lanterns: Map<string, LanternState>,illuminationMap: Map<string, boolean>reference,hubsIlluminatedcounter,onActivatecallback array.createIlluminationSystem(levelData)constructor — seeds lantern map fromlevelData.generation.hubsand restores prioractivatedstate fromlevelData.illuminationMap.tickIllumination(...)per-frame fill / drain / activation loop. Returns the hub activated this frame, or null.- Query helpers:
isHubLit,isSpokeLit,getLanternFill,isPlayerInLanternRange,getIlluminatedHubIds,getConnectedSpokes. setIllumination(...)debug / playground writer that forces lit state and recountshubsIlluminated.- Constants:
ACTIVATION_RADIUS_FRAC(0.35 of hub radius),DEFAULT_FILL_SECONDS(4),DRAIN_RATE(0.5 per second). - Private
lerphelper for fill-time interpolation.
READS FROM
Hub,Spoke,LevelConfigfrom../../data/level-config— hub geometry (x,y,r,id), spoke endpoint indices (a,b), andconfig.illuminationSpeedfor fill duration.LevelDatafrom./chunk-manager— precomputedgeneration.hubsarray and the sharedilluminationMapreference.- Player world position (
playerX,playerY) anddt/timepassed in by the caller each tick.
PUSHES TO
- The shared
illuminationMap: Map<string, boolean>(mutates it directly on activation and viasetIllumination). system.hubsIlluminatedcounter (incremented on activation, recomputed insetIllumination).system.onActivatecallbacks — invoked synchronously with(hubId, hub)when a lantern reaches fill 1.tickIlluminationreturn value — caller receives the freshly activated hub for that frame.
DOES NOT
- Does not render lanterns, fill meters, fog, or hub/spoke lighting visuals —
rendering/draw-hub-spoke.tsandrendering/hud.tsconsumeLanternState. - Does not modify spawn pools, event tables, or VFX directly — it only fires
onActivatecallbacks. Cascade wiring lives inbridge-hub-illumination.tsand the systems it touches (zone-classifier,events,event-spawner,zone-spawn-adapter). - Does not persist illumination across runs —
illuminationMapis per-LevelDataand rebuilt bychunk-manager. - Does not implement fog of war or dynamic point lights — lit / unlit is a binary per-hub flag, with spokes lit iff either endpoint is lit.
- Does not own
LevelConfig.illuminationSpeedsemantics beyond the fill-time mapping (0 = instant, 1 = slowest). - Does not de-activate lanterns under normal gameplay; only
setIllumination(..., false, ...)can unset, and only via debug paths.
Signals
onActivate: Array<(hubId, hub) => void>— fired once per hub the frame itsfillfirst reaches 1. Used by bridge wiring to cascade spawn-pool, event, and VFX changes.tickIlluminationreturn value —Hub | null; non-null only on the activation frame, used as a one-shot trigger by the caller.- Mutation of
illuminationMap— any subsystem holding the sameMapreference observes lit hubs immediately on the activation frame.
Entry points
createIlluminationSystem(levelData)— called during level boot inengine/bridge.ts(alongsidechunk-managersetup).tickIllumination(system, playerX, playerY, hubs, dt, time, config)— called every frame fromengine/bridge.tsin the world update step.- Query helpers imported by HUD, rendering, zone classification, and event spawning code.
setIllumination(...)invoked by playground tabs (screens/playground/VfxTab.tsx) and similar debug surfaces.
Pattern notes
- Plain-data interfaces plus free functions, no classes. State lives in
IlluminationSystem; behavior lives in exported functions that take it as the first argument. - Single source of truth for lit state is
illuminationMap(owned byLevelData).LanternState.activatedmirrors it for fast per-frame iteration.setIlluminationkeeps them in sync and re-derives the counter. - Fill-time curve is inverted:
illuminationSpeed0 yields ~0.01 s (effectively instant), and 1 yieldslerp(0.5, DEFAULT_FILL_SECONDS + 1, 1)= 5 s. Higher config value = slower fill, despite the name. - Drain runs at a fixed
DRAIN_RATE(0.5 / s) regardless ofilluminationSpeed, so leaving the circle costs less than re-filling it. - Activation radius is
hub.r * 0.35, not a constant world distance — larger hubs have larger lantern circles. - Spoke illumination is a derived query (
isSpokeLit) computed from hub endpoints; spokes carry no independent lit flag. - Constants block at top of file is the single place for tuning radii / timings; per-level pacing lives in
LevelConfig.illuminationSpeed. - Callbacks fire synchronously inside the tick loop — callers must not mutate
system.lanternsorilluminationMapfrom withinonActivatehandlers in ways that break the in-progress iteration. - No fog-of-war or visibility-mask data structures exist here; “darkness” in this codebase is rendered as styling differences between lit and unlit hubs/spokes, driven by the boolean map.