PURPOSE

Defines the data model and pure helpers for warp puddles — circular purple zones scattered on terrain at level-gen time. A simplified replacement (v5.95+) for the earlier fbm-perturbed ellipse model: plain circles with cheap containment tests, with overlapping circles merged into groups via union-find. Currently parked behind a kill-switch (WARP_PUDDLES_ENABLED = false) since 2026-04-21, with all code paths retained in-repo for future iteration.

OWNS

  • WarpPuddle interface — single circular zone with id, hubId, x, y, radius, colorSeed.
  • WarpPuddleGroup interface — merged set of overlapping puddles with id, members, axis-aligned bbox for coarse culling.
  • WARP_PUDDLES_ENABLED master kill-switch constant.
  • Spawn tuning constants: ANY_PUDDLE_CHANCE (0.30), SECOND_PUDDLE_CHANCE (0.10), MIN_RADIUS (120), MAX_RADIUS (280), MIN_PUDDLE_SEPARATION_MULT (1.1).
  • puddleContainsPoint — squared-distance circle containment test.
  • groupContainsPoint — bbox-rejected member-circle containment test.
  • findContainingGroup — linear scan over groups returning the first containing group or null.
  • groupPuddles — union-find over an O(n²) overlap matrix, returning merged groups with computed bboxes.
  • spawnPuddlesForHub — deterministic 0–2 puddle spawner per hub, gated by the kill-switch and the spawn-chance rolls.

READS FROM

  • Externally supplied deterministic rng: () => number (passed in by the level generator) for all spawn rolls — placement angle/distance, radius, second-puddle chance, color seed.
  • WARP_PUDDLES_ENABLED flag at the top of spawnPuddlesForHub to short-circuit to an empty array.
  • Hub parameters (hubId, hubX, hubY, hubRadius) supplied by the caller for placement.

PUSHES TO

  • Returns WarpPuddle[] from spawnPuddlesForHub to the level generator (consumed by the hub-spoke generator / chunk manager).
  • Returns WarpPuddleGroup[] from groupPuddles to whatever runtime owns the active group list for collision and rendering.
  • Provides boolean predicates and lookup results to runtime collision queries (e.g. ship.warpGroupId assignment when the player enters a group).

DOES NOT

  • Does not render anything. Visual treatment (fill all member circles, stroke only the outer-edge trick, glow, color animation phasing) lives in the renderer that consumes these structs.
  • Does not run per-frame logic, mutate any global state, or hold a registry — purely data + pure functions.
  • Does not couple to the ship, collision system, telemetry, or audio.
  • Does not despawn or recycle puddles — lifetime is governed by the owning chunk/hub.
  • Does not seed its own randomness — all stochastic choices flow through the injected rng.
  • Does not apply gameplay effects of being inside a puddle — the caller is responsible for mapping group containment to behaviour.

Signals

  • Return value of spawnPuddlesForHub (length 0, 1, or 2) signals to the level generator how many puddles were rolled for the hub.
  • puddleContainsPoint, groupContainsPoint, and findContainingGroup return booleans / group-or-null used by runtime systems to detect entry and resolve ship.warpGroupId.
  • Group id of the form wg:<firstMemberId> provides a stable identifier suitable for per-frame state comparisons.
  • Puddle id of the form <hubId>:wp<index> provides a stable, hub-scoped identifier for culling.
  • colorSeed per puddle is the signal to the renderer to phase-offset color/animation so neighboring puddles don’t lockstep.

Entry points

  • WARP_PUDDLES_ENABLED — re-exported feature flag read by external systems to skip all puddle work when off.
  • spawnPuddlesForHub(hubId, hubX, hubY, hubRadius, rng) — called once per hub at level-gen time.
  • groupPuddles(puddles) — called once after all puddles for a level/region are spawned to fold overlaps into groups.
  • puddleContainsPoint(p, px, py) — used by collision tests and rendering masks.
  • groupContainsPoint(g, px, py) — used for per-group containment checks with bbox pre-reject.
  • findContainingGroup(groups, px, py) — used to resolve which (if any) group a world-space point falls in.

Pattern notes

  • Pure data + pure functions module — no class instances, no hidden state, no side effects.
  • Determinism is contract: spawning takes an injected rng, so the same hub parameters and rng sequence always produce identical puddles.
  • Kill-switch is an early-return at the top of spawnPuddlesForHub; downstream functions still work on whatever inputs they receive, so flipping the flag back on requires no other code changes.
  • Containment tests use squared distance (dx*dx + dy*dy <= r*r) to avoid Math.sqrt.
  • Group containment does a coarse axis-aligned bbox reject before iterating members, exploiting the fact that most queried points are far from any group.
  • Grouping uses union-find with path compression. The O(n²) overlap scan is acceptable because n is bounded to a few dozen puddles per level.
  • Group bboxes are computed once at group-construction time from member centers ± radii and cached on the struct.
  • Group id derives from the first member’s id rather than a counter, keeping ids deterministic across runs.
  • Spawn placement uses a retry loop (up to 12 attempts) that respects MIN_PUDDLE_SEPARATION_MULT against already-accepted puddles on the same hub — but still allows enough overlap for grouping to kick in across hubs.
  • Offset distance is bounded to 0.5 * hubRadius, keeping puddles roughly inside the hub footprint.
  • Radius range (120–280 px) is deliberately tighter and smaller than the previous v1 range (200–600) to keep puddles from dominating the level.
  • Spawn probabilities are stacked: ANY_PUDDLE_CHANCE (0.30) gates any spawn at all; conditional on that, SECOND_PUDDLE_CHANCE (0.10) gates the pair case — net ~27% single, ~3% pair per hub.
  • Module-level rationale comment captures why the simplification happened (fbm needed identical eval on every collision test and shader pixel, made overlap gnarly, didn’t look better) — a deliberate trade of fidelity for cheapness and clarity.