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

+3 -4
View File
@@ -366,7 +366,7 @@ function matchReady() {
// stumble; the beat only lands if it is down long enough to look beaten.
// "fall quicker. lie there for 2s then stand up." The collapse is now a drop — two frames in a fifth
// of a second — and the time is spent on the floor, where it reads.
const FALL = [['fall1', 0.09], ['fall2', 0.11], ['fall3', 2.0], ['rise', 0.30]];
const FALL = [['fall1', 0.05], ['fall2', 0.07], ['fall3', 2.0], ['rise', 0.30]];
export const FALL_LEN = FALL.reduce((s, f) => s + f[1], 0);
function startFall(from) {
@@ -473,7 +473,7 @@ function matchPoint(side) {
// AFTER the score call, not under it. voice() drops any line that would start while the umpire
// is still talking (one man, one microphone) — and matchPoint calls scoreKey() at 0.4 s with a
// clip over a second long, so every one of these was being swallowed silently.
aside(G.hp <= 2 ? 'ump_low_blood' : (score.pts[1] % 2 ? 'ump_wound_you2' : 'ump_wound_you'), 2.2);
aside(G.hp <= 2 ? 'ump_low_blood' : 'ump_wound_you2', 2.2);
if (G.hp <= 0) return killPlayer(); // eaten — the run summary, as ever
}
const over = advancePoint(side); // banks the game and resets the points itself
@@ -484,7 +484,7 @@ function matchPoint(side) {
// A LIMB IS WORTH A REMARK. Two lines, alternating on the stage taken, so the same sentence
// never lands twice in one game — a repeated aside reads as a stuck HUD, the same reason the
// praise words cycle rather than roll.
aside(fellFrom ? 'ump_wound_opp2' : 'ump_wound_opp', 2.2); // ...after the score call, see above
aside('ump_wound_opp', 2.2); // ...after the score call, see above
return startFall(fellFrom); // it goes down, and gets up short of a limb
}
matchReady(); // next point, same opponent, both on their marks
@@ -500,7 +500,6 @@ function matchGameWon(zb, banked = false) {
G.runGames++; // the ladder metric, same as every mode
G.hp = Math.min(G.hpMax, G.hp + TUNE.matchHeal); // a clean game is how you last
voice('game_player', 0.3); cheer(2.4, 0.6);
aside('ump_felled', 2.8); // ...after 'game', over the applause
// GAME is the FLASH and the next opponent's name plate is the CARD. It used to be the other way
// round and matchOpponent's plate landed on the same slot a frame later, so 'GAME.' never drew
// once — and the plate is the better card anyway: it announces the win by replacing the loser.