Warp Puddle Effect
Warp puddles are world features — circular zones scattered on terrain — that override ship stats while the player ship is inside them. The effect ramps with a warpT value (0 outside, 1 fully inside) and snaps back to base on exit. They are timed buffs gated on location, distinct from artifacts (permanent run-long passives picked up once).
Status
Disabled. WARP_PUDDLES_ENABLED = false since 2026-04-21. Nate parked the experiment; code paths are kept in-repo for future iteration. No puddles spawn, no groups form, no collision tests fire, no rendering happens while the flag is off.
Stat patches while inside
Applied per-frame in applyWarpPuddleOverrides(ship) (engine/world/warp-puddle-effects.ts). All writes are absolute, from ship._base, not multiplicative compounding — at warpT = 0 the multiplier is 1.0 so values return to base on exit (idempotent).
| Stat | Override at warpT = 1 | Formula |
|---|---|---|
thrust | +25% | base.thrust * (1 + (1.25 - 1) * t) |
heatBurnRate | 0 (no heat builds) | base.heatBurnRate * (1 - t) |
heat (dynamic state) | drains toward 0 | heat *= (1 - t) per frame while t > 0 |
| Camera zoom | +20% | Handled in camera.ts |
THRUST_MULT_MAX = 1.25 is the tunable cap. Earlier versions read +5% in comments but the constant is the source of truth.
Accumulation safety
Previous versions multiplied ship.thrust *= N each frame. Because Modifiers.recalc(ship, _base) only runs on dirty frames, the multiplier compounded exponentially — after ~30 frames the ship moved 20× normal speed and never came back on exit. Current model writes absolute values from _base every frame, which is idempotent. Requires ship._base.thrust and ship._base.heatBurnRate to be snapshotted at mission start (see bridge.ts ~line 540).
Visual signature
Magic purple zones (per file header). Simplified rendering model from v5.95+: plain circles, not fbm-perturbed ellipses. Earlier fbm-perturbed edges:
- Needed an identical fbm eval on every collision test and shader pixel.
- Made overlap detection gnarly.
- Didn’t look demonstrably better than a ringed circle with a glow.
Each puddle carries a colorSeed (1–101) so neighboring puddles don’t lockstep their color/animation phase. Overlapping puddles render as a union (fill all member circles, stroke only the outer edge).
Spawn rules
Per-hub spawning in spawnPuddlesForHub(hubId, hubX, hubY, hubRadius, rng). Deterministic via the rng passed in.
| Constant | Value | Meaning |
|---|---|---|
ANY_PUDDLE_CHANCE | 0.30 | Chance any given hub spawns at least one puddle. |
SECOND_PUDDLE_CHANCE | 0.10 | Given a hub spawns one, chance it spawns a second. |
MIN_RADIUS | 120 px | Lower bound on puddle radius. |
MAX_RADIUS | 280 px | Upper bound (deliberately smaller than v1’s 200–600). |
MIN_PUDDLE_SEPARATION_MULT | 1.1 | Min separation between two puddles on the same hub, as multiples of the larger puddle’s radius. Allows overlap so grouping kicks in. |
Per puddle: 12 placement attempts at random offsets within hubRadius * 0.5 of the hub center, picking the first that satisfies the separation constraint. Returns empty array when the feature flag is off, regardless of rng.
Stable id format: ${hubId}:wp${i}.
Grouping (union-find merge)
Overlapping puddles merge into groups at level-gen time via union-find (groupPuddles(puddles)):
- Two puddles are “merged” if
centerDist² <= (r1 + r2)²— pure circle overlap. - O(n²) overlap check is fine: n is small (a few dozen per level max).
- Each group gets an axis-aligned bbox for coarse culling.
- Group id format:
wg:${firstMember.id}.
A group is the collision + rendering unit. When the player enters any member circle, ship.warpGroupId = group.id for as long as they stay inside any member.
Containment tests
puddleContainsPoint(p, px, py)— cheap circle test, used by collision and rendering mask.groupContainsPoint(g, px, py)— coarse bbox reject, then linear member scan.findContainingGroup(groups, px, py)— linear across groups (fine for ~10 visible groups per frame).
Distinct from artifacts
| Warp puddle | Artifact | |
|---|---|---|
| Trigger | Location (ship inside zone) | One-time pickup |
| Duration | While inside; ramps with warpT, snaps back on exit | Whole run after pickup |
| Stacking | Single zone overrides; absolute writes from _base | Run-long passive on ship._base |
| Source file | engine/world/warp-puddles.ts, engine/world/warp-puddle-effects.ts | engine/world/artifacts.ts |
| Current status | Disabled (flag off) | Active |
Source files
src/starship-survivors/engine/world/warp-puddles.ts— spawn, grouping, geometry, feature flag.src/starship-survivors/engine/world/warp-puddle-effects.ts— per-frame stat overrides while inside.