The timing cues were match-only, and the walk cycles bounced

CLASSIC TENNIS, third time asked and this time actually found. Both cues open
with a mode gate and I checked the wrong two features twice: drawApproachRing
(the closing circles that show when to tap) and drawApexBand (the serve bar
with the gold perfect window) each began `if (!G.match ...)`. So the mode whose
whole pitch is "if the core tennis is good everything else is good" was the one
mode with neither. They run on `G.match || G.mode === 'tennis'` now — the gore
mode stays out on purpose, there is no rally there. A test reads both function
bodies and fails if either is ever gated on the match alone again.

(The two I kept pointing at — the landing ring and the placement ring — really
are shared. They were just not the ones being asked about.)

THE BOUNCING WAS IN THE ART, and measurable: the six generated families came
back with the shuffle frame 30-44 px shorter than the stance it walks out of,
against 9 px on the hand-made racket family. The game draws every frame at its
own PNG height, so the body rose and dropped once per step. A walk cycle is one
drawing with the legs moved and must be one height; _zsheet levels the stance
and the stride pair now and leaves the forehand its own, exactly as the serve
and the fall keep theirs. All seven families re-cut: spreads are 1-8 px.

THE BENCH GETS BUTTONS. Arrow keys are not a control scheme on a phone, which
is the device the walk has to be judged on — so every family and every wound
stage was unreachable from the place the problem shows. Three on-screen pads:
BODY, WOUND, POSE, the two-way ones split left/right. Geometry lives beside the
drawing and input hit-tests the same function, the rule the rack established.

THE FALL is quicker still (0.05/0.07/2.0/0.30): drop, lie there, get up.

Three umpire lines pulled — ump_wound_you, ump_felled, ump_wound_opp2. Not
funny enough to keep. Files deleted, call sites collapsed to the two that stay.

60 tests green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 20:38:50 +02:00
1 parent e1ce2486c0
commit eb0e8775a4
46 files changed
+81 -8

No files matched your search

+16
View File
@@ -5,6 +5,7 @@
import { cv } from './dom.js';
import { audio, pock } from './audio.js';
import { TUNE, WEAPONS } from './config.js';
import { benchButtons, W, H } from './render.js'; // W/H are live bindings: fitViewport rewrites them
import { G, score } from './state.js';
import { keyPressed, dragServe, selectWeapon, serveToss, pullQuality, pullPerfect, dismissModal, slotId, dodge } from './gameplay.js';
import { rackHit } from './render.js';
@@ -106,6 +107,21 @@ cv.addEventListener('touchstart', e => {
// gun"). Only the TAP dies, decided at touchend: a drag or hold starting in the same spot is
// a deliberate gesture (burst steering parks the thumb low, serve pulls start anywhere) and
// must keep working, so the stick is still created.
// THE BENCH'S OWN BUTTONS, tested before anything else — the bench is a back-room art screen and
// its taps mean only one thing. Arrow keys are not a control scheme on a phone, which is exactly
// the device the walk cycles have to be judged on. Geometry comes from render (benchButtons), so
// a moved button cannot stop responding — the rack learned that lesson first.
if (G.bench) {
const bx = (t.clientX - r.left) / r.width * W, by = (t.clientY - r.top) / r.height * H;
let hit = false;
for (const b of benchButtons()) {
if (bx >= b.x && bx <= b.x + b.w && by >= b.y && by <= b.y + b.h) {
keyPressed(bx < b.x + b.w / 2 ? b.k : b.k2); // left half steps back, right half forward
hit = true; break;
}
}
if (hit) continue;
}
let nearRack = false;
if (!G.horde) { // rack tap selects a tin — a burst can be aborted out of, a horde cannot (selectWeapon)
const slot = rackHit((t.clientX - r.left) / r.width, (t.clientY - r.top) / r.height);