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

  • LanternState interface: per-hub fill 0-1, playerInRange flag, activated boolean, activatedAt timestamp, hubId.
  • IlluminationSystem interface: lanterns: Map<string, LanternState>, illuminationMap: Map<string, boolean> reference, hubsIlluminated counter, onActivate callback array.
  • createIlluminationSystem(levelData) constructor — seeds lantern map from levelData.generation.hubs and restores prior activated state from levelData.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 recounts hubsIlluminated.
  • Constants: ACTIVATION_RADIUS_FRAC (0.35 of hub radius), DEFAULT_FILL_SECONDS (4), DRAIN_RATE (0.5 per second).
  • Private lerp helper for fill-time interpolation.

READS FROM

  • Hub, Spoke, LevelConfig from ../../data/level-config — hub geometry (x, y, r, id), spoke endpoint indices (a, b), and config.illuminationSpeed for fill duration.
  • LevelData from ./chunk-manager — precomputed generation.hubs array and the shared illuminationMap reference.
  • Player world position (playerX, playerY) and dt / time passed in by the caller each tick.

PUSHES TO

  • The shared illuminationMap: Map<string, boolean> (mutates it directly on activation and via setIllumination).
  • system.hubsIlluminated counter (incremented on activation, recomputed in setIllumination).
  • system.onActivate callbacks — invoked synchronously with (hubId, hub) when a lantern reaches fill 1.
  • tickIllumination return 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.ts and rendering/hud.ts consume LanternState.
  • Does not modify spawn pools, event tables, or VFX directly — it only fires onActivate callbacks. Cascade wiring lives in bridge-hub-illumination.ts and the systems it touches (zone-classifier, events, event-spawner, zone-spawn-adapter).
  • Does not persist illumination across runs — illuminationMap is per-LevelData and rebuilt by chunk-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.illuminationSpeed semantics 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 its fill first reaches 1. Used by bridge wiring to cascade spawn-pool, event, and VFX changes.
  • tickIllumination return 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 same Map reference observes lit hubs immediately on the activation frame.

Entry points

  • createIlluminationSystem(levelData) — called during level boot in engine/bridge.ts (alongside chunk-manager setup).
  • tickIllumination(system, playerX, playerY, hubs, dt, time, config) — called every frame from engine/bridge.ts in 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 by LevelData). LanternState.activated mirrors it for fast per-frame iteration. setIllumination keeps them in sync and re-derives the counter.
  • Fill-time curve is inverted: illuminationSpeed 0 yields ~0.01 s (effectively instant), and 1 yields lerp(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 of illuminationSpeed, 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.lanterns or illuminationMap from within onActivate handlers 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.