← back to Wild Orbs Opus
Nova shockwaves chain; lock the cascade bounds in tests
81c668070775e4004a98650cc072201c942e3156 · 2026-09-08 16:13:51 -0700 · Steve Abrams
Measuring the nova cascade showed peak concurrent shockwaves = 1: dmg 1 could
never kill an hp-2 nova, so the type never chained into itself despite its own
description. Raised the shockwave to dmg 2 so it shatters what it crosses.
The recursion is bounded by construction (a shockwave damages each orb at most
once via its hit Set, and waves expire), which the new regression test now
asserts rather than assumes: a 20-nova cluster peaks at ~87 shockwaves, clears
the field, and settles to zero in ~61 frames with no errors.
Also replaced the fixed-sleep assertions with polling — they failed on a loaded
machine, measuring the load average instead of the game.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XP13P5ZKs7oWJQ3pjbvpnG
Files touched
M README.mdM index.htmlM test/smoke.mjs
Diff
commit 81c668070775e4004a98650cc072201c942e3156
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 8 16:13:51 2026 -0700
Nova shockwaves chain; lock the cascade bounds in tests
Measuring the nova cascade showed peak concurrent shockwaves = 1: dmg 1 could
never kill an hp-2 nova, so the type never chained into itself despite its own
description. Raised the shockwave to dmg 2 so it shatters what it crosses.
The recursion is bounded by construction (a shockwave damages each orb at most
once via its hit Set, and waves expire), which the new regression test now
asserts rather than assumes: a 20-nova cluster peaks at ~87 shockwaves, clears
the field, and settles to zero in ~61 frames with no errors.
Also replaced the fixed-sleep assertions with polling — they failed on a loaded
machine, measuring the load average instead of the game.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XP13P5ZKs7oWJQ3pjbvpnG
---
README.md | 4 ++--
index.html | 4 +++-
test/smoke.mjs | 29 +++++++++++++++++++++++++++--
3 files changed, 32 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 98b02af..8cc6d11 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ plus on-screen PULSE / WARP buttons.
| **CHAIN BOMB** | detonates every orb nearby on a staggered fuse — cascades hard |
| **MIRROR** | clones itself twice (twice), then finally breaks |
| **RUBBER** | never wraps — bounces off the walls 50% faster on every hit, 5 hits to pop |
-| **NOVA** | erupts an expanding shockwave ring that shatters whatever it crosses |
+| **NOVA** | erupts a shockwave ring that shatters whatever it crosses — and chains into other novas |
| **SENTINEL** | deploys tracking turrets that shoot back |
| **WARP** | rips space open and swaps *your ship* into the orb's position |
@@ -88,7 +88,7 @@ bestiary, touch and gamepad support.
## Tests
```bash
-npm test # headless: 55 assertions
+npm test # headless: 57 assertions
npm run test:headed # watch it run, writes screenshots to test/shots/
npm run profile # per-phase render timings + sustained fps
```
diff --git a/index.html b/index.html
index e6da029..ac55310 100644
--- a/index.html
+++ b/index.html
@@ -1162,7 +1162,9 @@ function destroyOrb(o, silentCombo){
}
/* 11. NOVA — expanding shockwave that shatters what it crosses */
case 'nova': {
- spawnShockwave(o.x,o.y, (240 + o.tier*70)*M, 1, 7.5, '#ffb03a');
+ // dmg 2 so it genuinely shatters what it crosses — and so novas chain
+ // into each other. Bounded: a wave damages each orb at most once.
+ spawnShockwave(o.x,o.y, (240 + o.tier*70)*M, 2, 7.5, '#ffb03a');
burst(o.x,o.y,34,'#fff3c4',8,{life:rnd(56,22)});
flashScreen('#ffb03a',.30); shake(15); hitStop(3);
floatText(o.x,o.y,'NOVA!','#ffd88a',22);
diff --git a/test/smoke.mjs b/test/smoke.mjs
index aeca2d2..df0defc 100644
--- a/test/smoke.mjs
+++ b/test/smoke.mjs
@@ -140,8 +140,33 @@ async function waitFor(page, fn, timeout = 12000, arg) {
}
destroyOrb(orbs[orbs.length - 1]);
});
- await page.waitForTimeout(2500);
- check('24-orb chain cascade resolves', errors.length === before, errors[before]);
+ const resolved = await waitFor(page, () => !orbs.some(o => o.chainT >= 0), 8000);
+ check('24-orb chain cascade resolves', resolved && errors.length === before, errors[before]);
+ }
+
+ /* ---------- 4b. nova chain-reaction bounds ----------
+ A nova shockwave kills orbs, and a dying nova spawns another shockwave.
+ That is deliberately recursive, so assert it stays bounded and settles. */
+ {
+ const before = errors.length;
+ const nova = await page.evaluate(async () => {
+ ship.inv = 1e9;
+ orbs.length = 0; waves.length = 0;
+ for (let i = 0; i < 20; i++) orbs.push(makeOrb(innerWidth/2 + Math.cos(i)*70, innerHeight/2 + Math.sin(i)*70, 3, 'nova'));
+ destroyOrb(orbs[0]);
+ let peakWaves = 0, peakOrbs = 0, f = 0;
+ while (f++ < 900) {
+ peakWaves = Math.max(peakWaves, waves.length);
+ peakOrbs = Math.max(peakOrbs, orbs.length);
+ await new Promise(r => requestAnimationFrame(r));
+ if (waves.length === 0 && f > 60) break;
+ }
+ return { peakWaves, peakOrbs, settled: waves.length, frames: f };
+ });
+ check('nova chain reaction actually chains', nova.peakWaves > 5, `peak ${nova.peakWaves} shockwaves`);
+ check('nova chain reaction stays bounded and settles',
+ nova.settled === 0 && nova.peakWaves < 400 && nova.peakOrbs < 200 && errors.length === before,
+ `peak ${nova.peakWaves} waves / ${nova.peakOrbs} orbs, settled in ${nova.frames} frames`);
}
/* ---------- 5. player abilities ---------- */
← 9854c68 Wild Orbs: rebuild from the Opus artifact — 13 orb types, te
·
back to Wild Orbs Opus
·
Verify the touch + cross-browser claims; fix the pointer-cap e78346c →