Zone Grid
The zone grid is a worldgen-emitted spatial lookup that assigns every cell of the precomputed world to one of five zone types. It is the structural map the spawner uses to decide what enemies appear at any (x, y) and at what rate. Built once during the BAKE phase of mission load, queried every frame at runtime.
What it is
A flat row-major array of ZoneType values covering a rectangular region of world space, addressable by world coordinates via a fixed cell size. Each cell stores structural zone only (which kind of place this is) — illumination state (lit vs dark) is layered on at lookup time from the live illuminationMap.
The five zone types:
| Zone | When a point falls here |
|---|---|
wilds | Outside every hub circle and farther than spokeHalfWidth from every spoke center line. The default. |
spoke_dark | Within spokeHalfWidth of a spoke center line where neither endpoint hub is lit. |
spoke_lit | Same geometry as spoke_dark, but at least one endpoint hub is illuminated. |
hub_dark | Inside a hub circle (distance <= hub.r) where that hub is not illuminated. |
hub_lit | Inside a hub circle where the hub is illuminated. |
Hub > spoke > wilds in priority: a point inside both a hub and a spoke classifies as the hub.
How it is built
bakeLevel in engine/world/chunk-manager.ts runs once at mission load:
- Generates all hubs and spokes (precomputed or scripted mode).
- Computes world bounds — either the bounding box of all scripted hubs plus 2000px padding, or
gridRadiuschunks in each direction from spawn. - Computes
spokeHalfWidth = lerp(spokeWMin, spokeWMax, spokeWidth) / 2. - Calls
buildZoneGridwith cellSize = 64px (trades spatial precision for 16× fewer cells than the default 16px).
buildZoneGrid in engine/world/zone-classifier.ts walks every cell, sampling the center point, and calls classifyZone against an empty illumination map (all hubs dark at build time). The result is stored as ZoneGrid:
interface ZoneGrid {
cellSize: number; // 64
originX, originY: number;
cols, rows: number;
grid: ZoneType[]; // row-major: grid[row * cols + col]
}
The grid lives on LevelData.zoneGrid for the entire run.
How it is queried
lookupZone(px, py, zoneGrid, hubs, spokes, illuminationMap) is the per-frame entry point. It:
- Maps
(px, py)to a cell index. Returnswildsif out of precomputed bounds. - Reads the structural zone from the flat array — O(1).
- If wilds, returns immediately.
- If hub_*, scans hubs to find which hub the point is in and resolves dark/lit from the live
illuminationMap. - If spoke_*, scans spokes with a bounding-box prefilter to find the containing spoke and resolves dark/lit from its endpoint hubs.
The spawner consumes this via querySpawnZone in engine/enemies/zone-spawn-adapter.ts, which returns the matching ZonePool from LevelConfig.spawnZones — base spawn rate and weighted enemy pool — for the AI Director to multiply against.
Why precompute it
Per-point zone classification without a grid is O(hubs + spokes) per query — scanning every hub circle and every spoke segment. The spawner queries zones every frame for every potential spawn position. Precomputing collapses the structural test to a single array index. Illumination state is the only thing checked live, because it changes when the player lights a lantern.
Wired by
- Spawner (
zone-spawn-adapter.ts) — pool + base rate routing per spawn position. - Terrain generator (
engine/world/generation.ts) — filters terrain placement to wilds whenpathableWildsis on. - Bridge debug overlays (
engine/bridge-debug-overlays.ts) — visualizes the grid for the playground LevelTab.
Cross-refs
enemy-spawn-zones.md— the fiveSpawnZoneConfigpools each zone routes to.hub-reveal-levels.md— how illumination state flips, which drives*_dark→*_littransitions.director-pressure.md— the spawn-rate multiplier applied on top of zone base rates.