PURPOSE
Classifies any world point into one of five biome-level zone types (hub_dark, hub_lit, spoke_dark, spoke_lit, wilds) and provides a precomputed spatial grid for fast runtime lookups. This is the structural foundation under sealed-arena hub illumination, spoke corridors, and the open wilds; gameplay systems (spawn rules, lighting, props) ask this module “what kind of place is this point in?“.
OWNS
- The
classifyZonepriority rule: hub > spoke > wilds. - The point-to-segment distance test used to detect spoke membership (
pointToSegmentDistSq). - The
ZoneGriddata shape: cell size, world origin, cols/rows, and a row-major flatZoneType[]array. buildZoneGrid: one-shot precomputation that snapshots the structural zone (hub/spoke/wilds) at each cell center, with all hubs treated as dark.lookupZone: runtime grid lookup that returns the structural cell value and resolves illumination live against the caller’silluminationMap.- The “out of precomputed bounds returns
wilds” rule inlookupZone. - The bounding-box prefilter (
±50world units around a spoke’s hub endpoints) used to skip spokes during runtime lookup.
READS FROM
HubandSpokeshapes and theZoneTypeunion, imported fromsrc/starship-survivors/data/level-config.ts.- Hub fields:
x,y,r,id. - Spoke fields:
a,b(indices into thehubsarray). - The caller-supplied
illuminationMap: Map<string, boolean>keyed byhub.id. - The caller-supplied
spokeHalfWidth(half the spoke corridor width in world units).
PUSHES TO
- Returns a
ZoneTypevalue to callers; does not mutate any input. - Returns a freshly allocated
ZoneGridfrombuildZoneGrid. - Consumed by
engine/world/chunk-manager.ts(callsbuildZoneGridat chunk/level load). - Consumed by
engine/world/generation.ts(callslookupZone). - Consumed by
engine/enemies/zone-spawn-adapter.ts(callslookupZoneto gate enemy spawn rules by zone).
DOES NOT
- Does not decide which hubs are illuminated; illumination state is owned elsewhere and passed in.
- Does not store or mutate
illuminationMap; it only reads it. - Does not handle scripted-mode hubs or
ScriptedHubshapes directly; it operates on the runtimeHub/Spoketypes only. - Does not render anything, emit telemetry, or play audio.
- Does not allocate during
classifyZoneorlookupZone(the hot paths); allocation happens only inbuildZoneGrid. - Does not treat wilds as ever illuminated; wilds is always returned as a single value with no lit/dark variant.
- Does not handle ties when a point sits inside multiple overlapping hubs or spokes beyond first-match-wins in array order.
Signals
ZoneTypereturn value:'hub_dark','hub_lit','spoke_dark','spoke_lit', or'wilds'.- A spoke is reported
spoke_litif either of its two endpoint hubs is illuminated, otherwisespoke_dark. - A point inside a hub’s radius is reported
hub_litifilluminationMap.get(hub.id)is truthy, otherwisehub_dark. - Out-of-bounds lookups against a
ZoneGridreturn'wilds'. - Structural fallbacks: if a cell was classified as a hub but no hub matches at runtime,
lookupZonereturns'hub_dark'; if classified as a spoke but no spoke matches, it returns'spoke_dark'.
Entry points
classifyZone(px, py, hubs, spokes, spokeHalfWidth, illuminationMap)— direct, ungrided classification; iterates hubs then spokes.buildZoneGrid(hubs, spokes, spokeHalfWidth, worldMinX, worldMinY, worldMaxX, worldMaxY, cellSize = 16)— builds and returns aZoneGrid; called once at level load.lookupZone(px, py, zoneGrid, hubs, spokes, illuminationMap)— fast runtime lookup against a precomputed grid; resolves illumination live.ZoneGridinterface — exported shape for callers that cache or pass the grid around.
Pattern notes
- Two-stage design: structural classification is precomputed and cached into a row-major grid (
grid[row * cols + col]); illumination is resolved live per query so toggling a hub’s lit state needs no grid rebuild. - Sample-at-cell-center:
buildZoneGridclassifies the world point at the center of each cell (+0.5 * cellSize); cell-size choice trades memory for spatial precision near hub/spoke boundaries. - Squared-distance comparisons throughout (
dx*dx + dy*dy <= r*r,distSq <= spokeHWsq) to avoidMath.sqrt. - Priority order in
classifyZoneis “hubs first because they are smaller and more specific”; a point inside a hub that also lies within a spoke corridor resolves tohub_*. lookupZonere-walks the hub and spoke arrays only to identify which hub/spoke owns the cell so it can read live illumination; the heavy point-vs-geometry math is not redone for hubs, and for spokes it uses an axis-aligned bounding-box prefilter (±50world units) before a full segment check.buildZoneGridbuilds with an emptyilluminationMapso the cached values are strictly structural (hub_dark/spoke_dark/wilds);hub_litandspoke_litonly ever appear from live queries.- Cell-size default of 16 world units is documented inline against a worked example: a 20-by-20 chunk area at
chunkSize=200yields a 250-by-250 grid of about 62,500 cells (about 125 KB). - Pure functions, no module-level mutable state; safe to call from any system.