Stop story bosses' rush at the net

Oliver: the strawberry seller as act I's first boss is way too hard,
"or it should not cross the net". His charge is the one boss move
exempt from the court band: it runs to a strike line in front of you,
and contact is death. In the story every boss's rush now stops a metre
short of the net and withdraws as before, so the threat is kept and the
reach is not. The Physio in act IV is held the same way. Only the
Champion's MATCH POINT rush in act V stays, because it is the finale.
New test. v385, staging only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5.5 committed 2026-10-15 15:36:18 +02:00
1 parent 16d0532f97
commit 2c9c7b84fd
6 files changed
+45 -1

No files matched your search

+10
View File
@@ -636,6 +636,16 @@ Each phase ships to staging and gets a phone verdict, and tests ride along with
5. **LATER.** The metal bed (when Oliver has a track: start at the transformation, gore only) and Members' 5. **LATER.** The metal bed (when Oliver has a track: start at the transformation, gore only) and Members'
Night (option C). Night (option C).
## Session log: no story boss crosses the net (2026-10-15, v385, STAGING ONLY)
Oliver: *"strawberry seller as first endboss is way too hard — or it should not cross the net"*. The
seller is the arcade's chapter-9 design. His move is `charge`, the one style deliberately exempt from
the z band: it rushes to a strike line in front of you, and contact is death. He stays act I's
mid-boss. Every story boss now carries `holdNet` (`beats.startStoryBoss`): a rush stops
`TUNE.bossNetStop` (1 m) short of the net and then withdraws as before. The threat is kept, the reach
is not. That covers the Physio in act IV too. Only THE CHAMPION as act V's boss keeps his MATCH POINT
rush, because it is the finale. Test: the seller rushes at least twice in 40 s and never passes
netZ - 1; the champion is not held.
## Session log: THE STORY, Oliver's first test (2026-10-15, v384, STAGING ONLY) ## Session log: THE STORY, Oliver's first test (2026-10-15, v384, STAGING ONLY)
Oliver: *"after my first test it all made a good impression"*, plus five asks. Oliver: *"after my first test it all made a good impression"*, plus five asks.
- **The metal track** ("Dystopian Survival", 3:36). `assets/audio/metal-loop.mp3` is 0.5-214 s with the - **The metal track** ("Dystopian Survival", 3:36). `assets/audio/metal-loop.mp3` is 0.5-214 s with the
+1 -1
View File
@@ -1201,6 +1201,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
</div> </div>
</div> </div>
<span id="recLamp" hidden>REC</span> <span id="recLamp" hidden>REC</span>
<script type="module" src="src/main.js?v=384"></script> <script type="module" src="src/main.js?v=385"></script>
</body> </body>
</html> </html>
+3
View File
@@ -1268,6 +1268,9 @@ function startStoryBoss(kind) {
const act = G.story.act; const act = G.story.act;
G.story.fight = kind; G.story.fight = kind;
startBossFight(storyBossChapter(act, kind), storyBossArt(act, kind)); startBossFight(storyBossChapter(act, kind), storyBossArt(act, kind));
// no story boss crosses the net — except THE CHAMPION, whose MATCH POINT rush IS the finale
const b = zombies.find(z => z.variant === 'B');
if (b) b.holdNet = !(act === 5 && kind === 'end');
} }
// THE MID-BOSS IS DOWN: the checkpoint. The rack is topped back up to the act's stock (never down), // THE MID-BOSS IS DOWN: the checkpoint. The rack is topped back up to the act's stock (never down),
+1
View File
@@ -318,6 +318,7 @@ export const TUNE = {
bossZMax: 5.6, // you (~57 s at bossSpeed) rather than by you killing it — and every extra hp bossZMax: 5.6, // you (~57 s at bossSpeed) rather than by you killing it — and every extra hp
// above just made that MORE likely, not less. Only 'charge' breaks the band. // above just made that MORE likely, not less. Only 'charge' breaks the band.
bossChargeV: 4.2, // m/s the physio rushes at once it commits bossChargeV: 4.2, // m/s the physio rushes at once it commits
bossNetStop: 1.0, // m short of the net a STORY boss's rush stops (holdNet): the threat without the reach
bossChargeT: 2.2, // s ONE RUSH takes, start to arrival — the charge speed is derived from it and bossChargeT: 2.2, // s ONE RUSH takes, start to arrival — the charge speed is derived from it and
// from how far away it is (bossAI), so the run-up reads the same every time and // from how far away it is (bossAI), so the run-up reads the same every time and
// is long enough to see coming. Was 1.5 s at a flat 4.2 m/s, which only reached // is long enough to see coming. Was 1.5 s at a flat 4.2 m/s, which only reached
+4
View File
@@ -202,6 +202,10 @@ function bossAI(zb, dt) {
// turns and withdraws the instant it arrives. A rush that misses is over. // turns and withdraws the instant it arrives. A rush that misses is over.
zb.x += Math.sign(zb.lane - zb.x) * Math.min(Math.abs(zb.lane - zb.x), TUNE.bossStrafe * 2 * dt); zb.x += Math.sign(zb.lane - zb.x) * Math.min(Math.abs(zb.lane - zb.x), TUNE.bossStrafe * 2 * dt);
zb.z += zb.chargeV * dt; zb.z += zb.chargeV * dt;
// THE STORY's bosses keep their own half (Oliver, 2026-10-15: the strawberry seller as act I's first
// boss was "way too hard — or it should not cross the net"): the rush runs at you and stops short
// of the net, a threat you read and punish rather than an instant death. (beats.startStoryBoss)
if (zb.holdNet) zb.z = Math.min(zb.z, COURT.netZ - TUNE.bossNetStop);
if (zb.moveT <= 0) { zb.charging = false; zb.moveT = TUNE.bossChargeRest; } // spent: it turns and walks back if (zb.moveT <= 0) { zb.charging = false; zb.moveT = TUNE.bossChargeRest; } // spent: it turns and walks back
} else { } else {
// AND IT FALLS BACK. A rush that ended where it landed left the boss parked on top of you for // AND IT FALLS BACK. A rush that ended where it landed left the boss parked on top of you for
+26
View File
@@ -3812,6 +3812,32 @@ test('THE STORY: the help offer — fresher legs and a longer swing in the cup,
G.horde = null; G.horde = null;
}); });
test('THE STORY: a boss never crosses the net — the strawberry seller rushes and stops short (the champion still comes)', () => {
intoStory(1, 'chap');
for (let g = 0; g < cfg.STORY.midAt; g++) { storyGame(); G.modal = null; }
const b = zombies.find(z => z.variant === 'B');
assert.equal(b.art.name, 'THE STRAWBERRY SELLER');
assert.ok(b.holdNet, 'held to its half');
let maxZ = -99, rushes = 0, was = false;
for (let i = 0; i < 40 * 60 && zombies.includes(b); i++) {
G.modal = null; G.modalQ.length = 0; G.hp = G.hpMax; G.phase = 'pretoss'; G.hurtT = 1;
update(DT);
maxZ = Math.max(maxZ, b.z);
if (b.charging && !was) rushes++;
was = !!b.charging;
}
assert.ok(rushes >= 2, 'it still rushes: ' + rushes);
assert.ok(maxZ <= COURT.netZ - TUNE.bossNetStop + 1e-9, `it stops short of the net: max z ${maxZ.toFixed(2)}`);
assert.equal(G.mode, 'splatter', 'and it never reached you');
zombies.length = 0; endBossFight();
// the champion's finale keeps its rush
restartRun();
intoStory(5, 'mid');
for (let g = cfg.STORY.midAt; g < cfg.STORY.length; g++) { storyGame(); G.modal = null; }
const c = zombies.find(z => z.variant === 'B');
assert.equal(c.art.name, 'THE CHAMPION'); assert.ok(!c.holdNet, 'the champion still comes for you');
});
test('THE STORY: a barrel holds what the act has earned', () => { test('THE STORY: a barrel holds what the act has earned', () => {
intoStory(1, 'chap'); intoStory(1, 'chap');
const seen = new Set(); const seen = new Set();