PURPOSE

Bridges the precomputed zone grid with the existing GameMaster enemy spawner. The spawner picks WHERE to spawn (ring around camera); this adapter classifies that position’s zone and returns the appropriate enemy pool, base spawn rate, and director-driven multipliers. When no LevelConfig is active, callers receive null and fall back to the legacy spawner behavior unchanged.

OWNS

  • ZoneSpawnResult interface — zone type, base rate, and weighted pool returned for a queried spawn position.
  • querySpawnZone(worldX, worldY, levelData) — classifies a candidate spawn position and returns its zone config bundle.
  • pickFromPool(pool, roll) — weighted-random selector over { type, weight }[]; weights do not need to sum to any constant.
  • shouldSpawn(baseRate, directorMult, dt, roll) — per-frame probability check combining zone rate with the AI Director multiplier.
  • DirectorZoneInputs / DirectorZoneOutputs interfaces — the input snapshot and output knobs for the difficulty curve.
  • computeDirectorZoneOutputs(inputs) — the canonical 4-minute difficulty curve producing spawnRateMultiplier and eliteChance.
  • Internal helpers getZonePool, lerp, clamp.

READS FROM

  • ../../data/level-configHub, Spoke, ZoneType, SpawnZoneConfig, ZonePool types.
  • ../world/chunk-managerLevelData type (the precomputed bundle holding zoneGrid, generation.hubs, generation.spokes, illuminationMap, and config.spawnZones).
  • ../world/zone-classifierlookupZone for grid-based zone classification at a world position.
  • Director inputs supplied by the caller: run timer, player HP fraction, hubs illuminated, current player zone, recent damage.

PUSHES TO

  • Returns ZoneSpawnResult | null to the caller (the GameMaster spawner) — pool, base rate, and zone label for the requested position.
  • Returns DirectorZoneOutputs to the caller — spawnRateMultiplier (clamped 0.3–3.0) and eliteChance (clamped 0–0.3) to scale spawn cadence and elite roll.
  • Returns selected enemy type IDs (strings) from pickFromPool.

DOES NOT

  • Does not pick spawn positions; the GameMaster spawner still owns ring-around-camera placement.
  • Does not instantiate, register, or own enemy entities; only returns IDs and rates.
  • Does not mutate LevelData, the zone grid, the illumination map, or any director state.
  • Does not read time, RNG, input, or rendering — all randomness and dt are passed in by the caller.
  • Does not handle the elite roll itself; it only emits the eliteChance value.
  • Does not apply when levelData is null — returns null so callers can passthrough to legacy behavior.

Signals

  • ZoneSpawnResult{ zone: ZoneType, baseRate: number, pool: Array<{ type: string; weight: number }> }.
  • DirectorZoneInputs{ runTimer, playerHpFrac, hubsIlluminated, playerZone, recentDamage }.
  • DirectorZoneOutputs{ spawnRateMultiplier, eliteChance }.
  • ZoneType cases handled by getZonePool: wilds, spoke_dark, spoke_lit, hub_dark, hub_lit (the latter two map to hub_illuminated/spoke_illuminated keys in SpawnZoneConfig).

Entry points

  • querySpawnZone(worldX, worldY, levelData) — main spawner integration point; returns pool+rate for a candidate position or null.
  • pickFromPool(pool, roll) — pool resolution; caller supplies the random roll.
  • shouldSpawn(baseRate, directorMult, dt, roll) — per-frame gate combining zone base rate with director multiplier.
  • computeDirectorZoneOutputs(inputs) — director extension that produces the current frame’s rate multiplier and elite chance from the inputs snapshot.

Pattern notes

  • Pure module: no side effects, no module-level state, no I/O. All time, randomness, and game state arrive as arguments.
  • Passthrough fallback when levelData is null preserves the legacy spawner code path without branching on a feature flag.
  • Difficulty curve is normalized over a 240-second (4-minute) run via t = runTimer / 240 and assembled from piecewise linear segments using lerp.
  • Rate curve breakpoints: 0.5x at 0s, 1.0x at 30s, 1.2x at 120s, 1.5x at 180s, 2.0x at 210s, 3.0x at 240s.
  • Elite chance breakpoints: 0 until 60s, ramps to 0.10 by 120s, 0.15 by 180s, then 0.30 by 240s.
  • Pressure-relief clause: when playerHpFrac < 0.3 and recentDamage > 20, the rate multiplier is capped at 0.5x before the illumination scaling.
  • Illumination compensation: rate is scaled by 1 + (hubsIlluminated / 20) * 0.5, hardening remaining dark territory as the player claims hubs (assumes ~20 reachable hubs).
  • Outputs are hard-clamped: spawnRateMultiplier to [0.3, 3.0], eliteChance to [0, 0.3].
  • pickFromPool falls back to the last pool entry when floating-point error leaves target > 0 after iteration, so callers never receive undefined.