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 classifyZone priority rule: hub > spoke > wilds.
  • The point-to-segment distance test used to detect spoke membership (pointToSegmentDistSq).
  • The ZoneGrid data shape: cell size, world origin, cols/rows, and a row-major flat ZoneType[] 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’s illuminationMap.
  • The “out of precomputed bounds returns wilds” rule in lookupZone.
  • The bounding-box prefilter (±50 world units around a spoke’s hub endpoints) used to skip spokes during runtime lookup.

READS FROM

  • Hub and Spoke shapes and the ZoneType union, imported from src/starship-survivors/data/level-config.ts.
  • Hub fields: x, y, r, id.
  • Spoke fields: a, b (indices into the hubs array).
  • The caller-supplied illuminationMap: Map<string, boolean> keyed by hub.id.
  • The caller-supplied spokeHalfWidth (half the spoke corridor width in world units).

PUSHES TO

  • Returns a ZoneType value to callers; does not mutate any input.
  • Returns a freshly allocated ZoneGrid from buildZoneGrid.
  • Consumed by engine/world/chunk-manager.ts (calls buildZoneGrid at chunk/level load).
  • Consumed by engine/world/generation.ts (calls lookupZone).
  • Consumed by engine/enemies/zone-spawn-adapter.ts (calls lookupZone to 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 ScriptedHub shapes directly; it operates on the runtime Hub/Spoke types only.
  • Does not render anything, emit telemetry, or play audio.
  • Does not allocate during classifyZone or lookupZone (the hot paths); allocation happens only in buildZoneGrid.
  • 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

  • ZoneType return value: 'hub_dark', 'hub_lit', 'spoke_dark', 'spoke_lit', or 'wilds'.
  • A spoke is reported spoke_lit if either of its two endpoint hubs is illuminated, otherwise spoke_dark.
  • A point inside a hub’s radius is reported hub_lit if illuminationMap.get(hub.id) is truthy, otherwise hub_dark.
  • Out-of-bounds lookups against a ZoneGrid return 'wilds'.
  • Structural fallbacks: if a cell was classified as a hub but no hub matches at runtime, lookupZone returns '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 a ZoneGrid; called once at level load.
  • lookupZone(px, py, zoneGrid, hubs, spokes, illuminationMap) — fast runtime lookup against a precomputed grid; resolves illumination live.
  • ZoneGrid interface — 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: buildZoneGrid classifies 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 avoid Math.sqrt.
  • Priority order in classifyZone is “hubs first because they are smaller and more specific”; a point inside a hub that also lies within a spoke corridor resolves to hub_*.
  • lookupZone re-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 (±50 world units) before a full segment check.
  • buildZoneGrid builds with an empty illuminationMap so the cached values are strictly structural (hub_dark/spoke_dark/wilds); hub_lit and spoke_lit only 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=200 yields 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.