PURPOSE

Orchestrates the 5-phase level lifecycle for chunk-based level data. Bakes the full world once at mission load (hubs, spokes, zone classification, illumination, triggers) into a single LevelData blob, then exposes cheap per-frame queries that determine which chunks fall into the active, spawn, and sim range rings around the camera. Per-frame logic only reads precomputed data — no generation happens after load.

OWNS

  • ChunkState interface — per-chunk runtime state (key, cx, cy, terrainActive, simActive).
  • LevelData interface — full precomputed bundle (config, generation, zoneGrid, illuminationMap, eventCompletionMap, triggers, precomputeMs).
  • Range constants — ACTIVE_RANGE (2 chunks beyond viewport, widest), SPAWN_RANGE (1 chunk), SIM_RANGE (0.5 chunk, tightest).
  • SCRIPTED_CHUNK_SIZE — fixed 2000 px chunk size for scripted mode.
  • bakeLevel(config, spawnX, spawnY, gridRadius) — the BAKE entry point; returns a LevelData. Default gridRadius is 2 (a 5×5 grid = 25 chunks, ~15–50 hubs).
  • computeActiveChunks(levelData, cameraX, cameraY, viewW, viewH) — per-frame chunk-key sets for active / spawn / sim rings.
  • getVisibleHubs(levelData, activeKeys) and getVisibleSpokes(levelData, activeKeys) — chunk-keyed lookups.
  • findNearestHub(levelData, x, y) — linear scan to place the player near a hub at spawn.
  • Internal helpers: buildScriptedGeneration(config), _buildAutoTriggers(hubs, config), lerp(a, b, t).

READS FROM

  • ../../data/level-configLevelConfig, Hub, Spoke, PrecomputedWorld, ScriptedHub, ScriptedSpoke, LevelTrigger types and DEFAULT_LEVEL_CONFIG.
  • ./hub-spoke-genprecomputeWorld, calcChunkSize, GenerateResult (the procedural generator for worldMode === 'precomputed').
  • ./zone-classifierbuildZoneGrid, ZoneGrid (called with a 64 px cell size against generated hubs and spokes).
  • config.scriptedHubs / config.scriptedSpokes — explicit hub layouts for story missions.
  • config.worldMode — selects 'scripted' vs 'precomputed' generation path (defaults to 'precomputed').
  • config.triggers — designer-authored triggers, merged with auto-generated ones (config wins on ID collision).
  • config.spokeWMin, config.spokeWMax, config.spokeWidth — interpolated to derive the spoke half-width passed to buildZoneGrid.
  • performance.now() — used to measure bake duration into precomputeMs.

PUSHES TO

  • Returns a LevelData object that downstream systems hold for the entire run (zone grid, illumination map, event completion map, triggers list).
  • computeActiveChunks returns three Set<string> of chunk keys consumed by terrain rendering, event/spawn systems, and enemy AI tickers.
  • getVisibleHubs and getVisibleSpokes produce arrays consumed by the renderer and any hub-aware systems.
  • illuminationMap is initialized with every hub id set to false; downstream gameplay flips entries.
  • eventCompletionMap starts empty; downstream systems write completion state into it.

DOES NOT

  • Does not render anything — produces data only.
  • Does not spawn enemies or tick AI — only marks which chunks are in the sim ring.
  • Does not run any generation work per frame — all hub/spoke/zone computation happens inside bakeLevel.
  • Does not mutate LevelData after creation aside from the maps that downstream systems write to.
  • Does not place warp puddles in scripted mode (warpPuddles and warpPuddleGroups are returned empty).
  • Does not auto-generate dialogue triggers — only spawn_wave triggers on every other hub. Dialogue is designer-authored via config.triggers or playground BUILD MODE.
  • Does not perform any I/O, asset loading, or network calls.

Signals

None. Module exposes plain functions and types; no event bus, listeners, or callbacks. Downstream systems poll LevelData and the per-frame chunk sets.

Entry points

  • bakeLevel(config, spawnX, spawnY, gridRadius) — called once at mission load to produce LevelData.
  • computeActiveChunks(levelData, cameraX, cameraY, viewW, viewH) — called every frame by the main loop.
  • getVisibleHubs(levelData, activeKeys) — called every frame by hub-aware consumers.
  • getVisibleSpokes(levelData, activeKeys) — called every frame by spoke renderers.
  • findNearestHub(levelData, x, y) — called at spawn (and on demand) to place the player.

Pattern notes

  • Five-phase lifecycle is explicit in the file header: BAKE / GENERATE / POPULATE / SIMULATE / CULL. Phases 1–3 collapse into bakeLevel; phases 4–5 are driven by computeActiveChunks plus downstream readers.
  • Two world-generation modes share the same downstream shape (GenerateResult): 'precomputed' calls precomputeWorld from hub-spoke-gen; 'scripted' builds the same struct from explicit scriptedHubs / scriptedSpokes with a fixed 2000 px chunk size.
  • Zone-aware generation is precomputed once via buildZoneGrid at 64 px cell resolution — trades spatial precision for ~16× fewer cells, enabling cheap per-frame zone lookups.
  • World bounds for the zone grid are computed differently per mode: scripted uses hub-position extents plus 2000 px padding; precomputed uses the chunk grid (gridRadius chunks in each direction from the spawn-centered chunk).
  • Range rings are concentric — ACTIVE_RANGE (2) ⊃ SPAWN_RANGE (1) ⊃ SIM_RANGE (0.5). Each frame, computeActiveChunks rebuilds the three sets from camera position and viewport size; the math is O(ring area), not O(world).
  • Chunk keys are stringified as "cx,cy" (e.g. "3,-1") and used as Map / Set keys throughout.
  • Triggers merge two sources: every other hub gets an auto spawn_wave trigger via _buildAutoTriggers; designer-authored config.triggers take precedence on ID collision (auto-triggers with conflicting IDs are filtered out).
  • bakeLevel defaults are conservative for mobile: gridRadius = 2 keeps hub count low because EventManager ticks every hub event every frame. Target bake time is under 5 s on mobile and under 2 s on desktop.
  • Scripted mode validates referential integrity — throws if scriptedHubs is empty or if scriptedSpokes references an unknown hub id.
  • chunkHubIndices is built during generation and used by both getVisibleHubs and getVisibleSpokes for chunk-keyed hub lookups; spokes are included when either endpoint hub is in an active chunk.
  • findNearestHub is a linear O(hubs) scan — acceptable because hub counts are small (15–50 typical) and it’s called on demand, not per frame.
  • Performance timing (precomputeMs) is captured on the returned LevelData for diagnostics.