Gore opens in the gore, the theme comes back, and every mode gets a board

Splatter gore no longer makes you win a game of tennis first — the mode you
pick is the mode you get, with the man you came to play already turned. The
transition itself is untouched and still reachable; it just isn't a toll booth.

The menu theme could only ever be started, never brought back, so the first
run of a session silenced it for good while the court's ambience followed you
into the menu. Starting a bed now un-ducks it too.

Three world boards instead of one: kills, opponents felled, and games taken
off the CPU, each with its own name prompt and its own row per player.

...and the gore economy stops paying a barrel for every game won past game 16,
while the shamblers, the mix and the ball machines keep escalating past the
point they all used to stop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-15 13:53:42 +02:00
1 parent 9cc3ac54b3
commit 3cf3e029a4
11 files changed
+440 -88

No files matched your search

+69 -4
View File
@@ -84,7 +84,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
// ---- game modules (real code) ----------------------------------------------
const state = await import('./src/state.js');
const { G, P, O, score, ball, setupPoint } = state;
const { update, forceZombies, splatPoint, restartRun, flickTarget, keyPressed, classicGame, serveApexY, assistK, rallyFeel } = await import('./src/gameplay.js');
const { update, forceZombies, startGore, splatPoint, restartRun, flickTarget, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN, startNetVs, endNetVs } = await import('./src/beats.js');
const { mirror, netMsg, netTest, netTick } = await import('./src/net.js');
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
@@ -95,7 +95,9 @@ const { killPlayer } = await import('./src/zombies.js');
const { drillQueue, drillLegal, arrival, drillMult, DRILL_TYPES } = await import('./src/drill.js');
const cfg = await import('./src/config.js');
const { TUNE, bossArt, COURT, racketStage } = cfg;
const { read: readStats, check: statsCheck } = await import('./src/stats.js');
const statsMod = await import('./src/stats.js');
const audio = await import('./src/audio.js');
const { read: readStats, check: statsCheck } = statsMod;
const DT = 1 / 60;
function freshSplatter() {
@@ -849,6 +851,66 @@ test('an ordinary far winner keeps its ordinary ending, and the match plays on (
assert.equal(score.pts[0], pts + 2, 'the match plays on');
});
test('the menus own the theme, and hand the court back its silence', () => {
// Oliver, 2026-08-15: "after some games — or returning to the main menu via the close button —
// the background music does not play any more, but you sometimes still hear the court background
// noise." loopBed no-ops once a file is playing, so menuMusic() could only ever START the theme,
// never bring it BACK from the duck start() put it under — and nothing ducked the court's own
// beds at all. The menu was silent and noisy at once, permanently, from the first run onward.
const d = audio.bedTest.ducked, THEME = 'background-music.m4a',
AMB = 'tennis-ambient-loop.wav', ZOM = 'zombie-mode.m4a';
audio.menuMusic(); // on the menu
assert.ok(!d.has(THEME), 'the theme plays on the menu');
audio.menuDuck(true); audio.ambient(); audio.zombieMusic(); // ...a run starts
assert.ok(d.has(THEME), 'the theme hands the court over');
assert.ok(!d.has(AMB) && !d.has(ZOM), 'and the court gets its own beds');
audio.menuMusic(); // ...and the run ends, however it ended
assert.ok(!d.has(THEME), 'the theme is BACK — this is the bug');
assert.ok(d.has(AMB) && d.has(ZOM), '...and the court does not follow you into the menu');
audio.ambient(); audio.zombieMusic(); // ...and the NEXT run is not played in silence
assert.ok(!d.has(AMB) && !d.has(ZOM), 'a second run sounds like the first');
audio.menuMusic();
});
test('SPLATTER GORE opens in the gore — no tennis prologue to win first', () => {
// Oliver, 2026-08-15: "it should not have the normal tennis intro anymore, we should just start
// right in splatter mode." The transition is not deleted (forceZombies still plays it) — what is
// gone is it standing between the menu and the mode.
restartRun();
assert.equal(G.mode, 'tennis', 'a fresh run starts where every run starts');
startGore();
assert.equal(G.mode, 'splatter', 'the mode you picked is the mode you get');
assert.ok(G.everTransitioned, '...and the prologue can never fire behind it');
assert.ok(zombies.some(z => z.variant === 'c'), 'the man you came to play is already one of them');
assert.ok(G.hideO, 'so the tennis opponent is gone from the far court');
assert.equal(score.server, 'P', 'and the serve is yours');
startGore();
assert.equal(zombies.filter(z => z.variant === 'c').length, 1, 'a second call is a no-op, not a second corpse');
});
test('the gore economy: barrels stop being income, and the late court keeps getting worse', () => {
// "in splatter gore it now gets too easy to just play through... maybe we should reduce the
// barrels again — pretty early in the game every won game gives a barrel." At 3/8 the gap hit ONE
// at game 16 and stayed, so the back half of a run paid a weapon per game won. The floor is the
// fix, and it is the property worth pinning: a barrel is a reward, never an income.
const gapAt = g => { G.runGames = g; return barrelGap(); };
for (const g of [0, 5, 12, 20, 40, 120])
assert.ok(gapAt(g) >= 2, `game ${g}: a barrel is never one-per-game (gap ${gapAt(g)})`);
assert.ok(gapAt(40) <= gapAt(0), 'the late game still earns them faster than the early one');
assert.equal(gapAt(400), TUNE.barrelFloor, '...down to the floor and no further');
// ...and the pressure keeps climbing where the barrels stop
const capAt = g => { G.runGames = g; return machCap(); };
assert.equal(capAt(0), TUNE.machMax, 'game one gets the old two guns');
assert.ok(capAt(40) > capAt(0), 'a long run faces more of them');
assert.equal(capAt(400), TUNE.machMaxCap, '...to a ceiling, because the court still has to read');
// the two ramps that used to stop dead around game 25 now run past it
assert.ok(1 + Math.min(TUNE.zSpeedMax, 30 * TUNE.zSpeedPerGame)
> 1 + Math.min(TUNE.zSpeedMax, 22 * TUNE.zSpeedPerGame), 'shamblers keep gaining speed past game 22');
assert.equal(Math.min(TUNE.nastyMax, 40 * TUNE.nastyPerGame), 1,
'and by the end every body that walks out is off the advanced roster');
G.runGames = 0;
});
test('THE TOURNAMENT: bands draw, bodies deepen, rounds hold their body, the champion ends it', () => {
const { CUP_ROUNDS } = cfg;
_startMatch(true); G.modal = null; G.modalQ.length = 0;
@@ -1148,7 +1210,7 @@ test('the toss strike has stakes: flush is fast, a miss is slow, the old free wi
test('classic tennis: the second set ends the run, with no corpse', () => {
G.mode = 'tennis'; G.classic = true;
G.sets = [1, 0]; score.games = [6, 0]; G.runGames = 14;
G.sets = [1, 0]; score.games = [6, 0]; G.runGames = 14; G.classicWon = 12;
assert.equal(classicGame('P'), true, 'the match is over');
assert.ok(G.done && G.done.won, 'and you won it');
assert.notEqual(G.mode, 'eaten', 'nobody died — this is the other ending');
@@ -1156,7 +1218,10 @@ test('classic tennis: the second set ends the run, with no corpse', () => {
const r = runs[runs.length - 1];
assert.equal(r.m, 'c', 'banked as a classic run');
assert.equal(r.dead, 0, 'a match WON is not a death — that is how wins are counted');
assert.equal(r.s, 2, 'sets won ride in the second headline slot');
// GAMES YOU WON, not sets: it is what classic's world board ranks, because every win is two sets
// and a board of nothing but 2s is a wall of ties. Nothing ever displayed the old value.
assert.equal(r.s, 12, 'games won ride in the second headline slot');
assert.equal(statsMod.BOARDS.c.score(r), 12, "...and that is the classic board's number");
});
test('classic tennis: losing the match ends it the same way, and counts as a loss', () => {