Seven verdicts from the test group

- opening CPU beatable in a game or two (softer cap, more whiffs, rubber band)
- modals are for calm courts only; live-court news is a card now
- the transition howto is gone: a ghost hand teaches the pull, the toss,
  the apex and the swing, each on the exact struggle it answers
- a shot barrel keeps your weapon and glows its slot instead
- switching weapons clicks, the live tile is unmistakable
- barrels come faster the deeper the run goes
- the umpire triple-tap cheat is deleted

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-08 19:49:27 +02:00
1 parent f13cbef28a
commit a89e8ff419
10 files changed
+242 -79

No files matched your search

+22 -14
View File
@@ -6,7 +6,7 @@
import { TUNE, UI, COURT, CONTACT, BOSS_ART, WEAPONS, BARREL_WEAPONS, woundStage, zombieH, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R } from './config.js';
import { G, P, ball, sendBoy, setupPoint } from './state.js';
import { splat, moan, pock, thud, zap, voice, boom, swish, shriek, aww, sfx , whoosh } from './audio.js';
import { splatPoint, cheer, spentBall, resolveInterlude, showModal, endBossFight, aside, grantWeapon, weaponModal, endBurst } from './gameplay.js';
import { splatPoint, cheer, spentBall, resolveInterlude, endBossFight, aside, grantWeapon, endBurst } from './gameplay.js';
import { tally, multiKill, endRun } from './stats.js';
export const zombies = []; // {x, z, v, walkT, variant: 'a'|'b'|'c'}
@@ -89,10 +89,13 @@ function pickBarrelWeapon() {
// upside — it never attacks, never scores, and rolls off on its own if you ignore it.
export function spawnBarrel() {
spawnZombie((Math.random() * 2 - 1) * 3.2, 4.5 + Math.random() * 3, 'o');
// a CARD, not a modal: it lands at a game-over beat that can have a standing wave on court, and
// a world-stopping popup there is exactly the "popups get me killed" report. Same for the
// machine's and the racket zombie's introductions below.
if (!G.seen.barrel) {
G.seen.barrel = true;
showModal({ title: 'A GLOWING BARREL', color: UI.pink,
body: ['THERE IS A WEAPON INSIDE IT', 'SHOOT IT AND IT IS YOURS', 'PINK MEANS TAKE THE SHOT'] });
G.card = { title: 'A GLOWING BARREL', sub1: 'A WEAPON IS INSIDE — SHOOT IT',
sub2: 'PINK MEANS TAKE THE SHOT', color: UI.pink, t: 2.6 };
}
}
@@ -103,8 +106,8 @@ export function spawnMachine() {
voice('ump_machine', 0.4, 0.95);
if (!G.seen.mach) {
G.seen.mach = true;
showModal({ title: 'A BALL MACHINE', color: UI.warm,
body: ['IT FIRES AT YOU ON A TIMER', 'SWING TO RETURN THE SHOT', 'OR SWISH TO BAT IT AWAY'] });
G.card = { title: 'A BALL MACHINE', sub1: 'IT FIRES AT YOU ON A TIMER',
sub2: 'SWING TO RETURN — OR SWISH IT AWAY', color: UI.warm, t: 2.6 };
}
}
@@ -349,19 +352,24 @@ const GORE_MUL = { a: 1, b: 1, e: 1, f: 1.8, s: 2.5, h: 2.5, g: 4, k: 5, r: 3, m
// one synchronous frame: nothing interleaves between the stamp and the kill it causes.
let CAUSE = 'ball';
// A barrel bursts and hands you what was inside, armed. Pink debris rather than gore — it is a
// container, not a body — and the modal only fires the FIRST time you see each weapon, so the tenth
// rocket does not stop the world to explain itself.
// A barrel bursts and hands you what was inside. Pink debris rather than gore — it is a container,
// not a body. It does NOT equip itself: shooting a barrel mid-plan used to yank the gun into your
// hand and the next serve fired the wrong thing — the current weapon stays live and the new slot
// pulses pink instead (render.drawRack reads G.rackGlow). The announcement is a CARD, never a
// modal: a barrel pops mid-combat, and a popup that stops the world there costs lives, not reads.
function popBarrel(zb) {
boom(0.7); G.shake = { t: 0.22, mag: 4 }; G.boomFlash = 0.08;
for (let k = 0; k < 30; k++) parts.push({
x: zb.x, y: 0.7, z: zb.z,
vx: (Math.random() * 2 - 1) * 4, vy: 1.2 + Math.random() * 3.5, vz: (Math.random() * 2 - 1) * 4,
t: 0.35 + Math.random() * 0.5, pink: true });
grantWeapon(zb.wid);
// first sight only — the gate lives in weaponModal, for both callers — and the flash keeps quiet
// when the modal spoke, so the same weapon isn't named twice in the same beat
if (!weaponModal(zb.wid)) G.flash = { text: WEAPONS[zb.wid].label, t: 1.1 };
const keep = G.weapon;
const got = grantWeapon(zb.wid);
G.weapon = keep;
G.rackGlow = { slot: got, t: 2.2 };
const w = WEAPONS[zb.wid];
G.card = { title: w.label, sub1: (w.desc && w.desc[0]) || '', sub2: 'TAP ITS RACK SLOT TO ARM IT',
icon: w.icon, color: UI.pink, t: TUNE.tinCardT };
}
// dark smoke: one puff, drifting up and spreading. Not gravity-bound like gore (see the parts loop).
@@ -671,8 +679,8 @@ export function spawnRacketZombie() { // one takes up residence on the far court
hp, hpMax: hp });
if (!G.seen.racket) { // it is not a shambler and it does not behave like one — say so, once
G.seen.racket = true;
showModal({ title: 'IT HAS A RACKET', color: UI.hot,
body: ['IT RETURNS YOUR WEAK SHOTS', 'ONLY A FULL PULL WOUNDS IT'] });
G.card = { title: 'IT HAS A RACKET', sub1: 'IT RETURNS YOUR WEAK SHOTS',
sub2: 'ONLY A FULL PULL WOUNDS IT', color: UI.hot, t: 2.6 };
}
}
function endHorde() {