Drop the dots, and stop handing back the dodge

"what are the dice dots for?" -> "drop them", and "the dodge swipe should not
reset to the middle when firing".

The pips were colourblind redundancy so the mode did not rest on hue alone,
but they were noise to everyone who can separate the colours and they shrank
to nothing on a body at distance — exactly where the channel had to work.
Gone from the bib and the chip. Recorded in PROJECT.md as a known gap rather
than pretended away: if the mode ships, the answer is bibs that differ by
shape at sprite scale, not dots.

The dodge one was never a drill bug. The auto-assist recovered toward HOME.P
including its x, so firing walked you back to the centre line: measured, a
step out to 1.94 m was dragged to 0.77 m over one ball's flight — most of a
dodge, handed back for free, which is the welded-in-place complaint the
sidestep exists to answer arriving slowly instead of all at once. Splatter
recovers in DEPTH ONLY now and holds whatever lane you stepped to. Tennis
keeps the full recovery: recovering to centre is the real sport's footwork
and there is no sidestep there to protect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-11 19:17:20 +02:00
1 parent 57357c7865
commit 586e8d4e4b
5 files changed
+49 -42

No files matched your search

+18
View File
@@ -356,6 +356,13 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
±4 m either side of the centre line at the baseline, so the walk bound alone let him leave the ±4 m either side of the centre line at the baseline, so the walk bound alone let him leave the
frame entirely. `xLimit()` in gameplay — the camera limit, floored at the sideline so every legal frame entirely. `xLimit()` in gameplay — the camera limit, floored at the sideline so every legal
ball stays catchable. The walk and the sidestep share it. ball stays catchable. The walk and the sidestep share it.
- **The auto-assist recovers in DEPTH ONLY in splatter — it must never undo a sidestep.** The
recovery target used to be `HOME.P`, x included, so firing walked you back toward the centre line:
measured, a step out to 1.94 m was dragged to 0.77 m over one ball's flight, i.e. most of a dodge
you had spent a cooldown on, handed back for free. That is the "welded in place" complaint the
sidestep exists to answer, arriving slowly instead of all at once. **Tennis keeps the full
recovery** — recovering to centre is the real sport's footwork and there is no sidestep there to
protect. (Oliver: *"the dodge swipe should not reset to the middle when firing"*.)
- **The sidestep is how you move outside `rally`, and it is not a walk.** One committed step - **The sidestep is how you move outside `rally`, and it is not a walk.** One committed step
(`TUNE.dodgeDist`) on a cooldown, carrying `TUNE.dodgeIFrame` seconds that beat BOTH damage (`TUNE.dodgeDist`) on a cooldown, carrying `TUNE.dodgeIFrame` seconds that beat BOTH damage
channels — the chip damage in `hurtPlayer` and the instant-death contact test. It exists because a channels — the chip damage in `hurtPlayer` and the instant-death contact test. It exists because a
@@ -598,6 +605,17 @@ are the ones being called now, which is a better decoy than the old fixed-batch
established soak trick; and never compare `zombies[i]` across frames, because bodies despawn at the established soak trick; and never compare `zombies[i]` across frames, because bodies despawn at the
baseline and the array shifts (that read as a 1.5 m lane drift that never happened). baseline and the array shifts (that read as a 1.5 m lane drift that never happened).
**FIFTH PASS — two cuts.** *"what are the dice dots for?"* -> *"drop them"*, and *"the dodge swipe
should not reset to the middle when firing"*.
- **The die-face pips are gone from the bib and the HUD chip.** They were colourblind redundancy —
a channel so the mode did not rest on hue alone — but they were noise to everyone who can separate
the colours, and they shrank to nothing on a body at distance, which is exactly where the channel
had to work. **This is a known accessibility gap now, recorded rather than solved**: if the mode
ships, the answer is bibs that differ by SHAPE or marking at sprite scale, not dots. A channel that
fails at the far baseline is not a channel.
- **The dodge fix is in gameplay.js, not in the drill**, because it was never a drill bug — see the
new invariant above. It changes the gore mode too, and deliberately.
**Open, and every one of them is Oliver's call after he plays it:** **Open, and every one of them is Oliver's call after he plays it:**
1. **A ball that carries through the lane can complete two steps at once** — measured: one serve 1. **A ball that carries through the lane can complete two steps at once** — measured: one serve
killed bib 1 and then bib 2 standing behind it, banking two correct steps in one shot. Had the killed bib 1 and then bib 2 standing behind it, banking two correct steps in one shot. Had the
+1 -1
View File
@@ -399,6 +399,6 @@
</div> </div>
</div> </div>
</div> </div>
<script type="module" src="src/main.js?v=155"></script> <script type="module" src="src/main.js?v=156"></script>
</body> </body>
</html> </html>
+20 -28
View File
@@ -16,41 +16,33 @@
// //
// THE MODE TRANSITIONS ARE NOT HERE — startDrill/endDrill live in beats.js with every other one // THE MODE TRANSITIONS ARE NOT HERE — startDrill/endDrill live in beats.js with every other one
// (CLAUDE.md rule 3). This file is the stream, the round and the rules. It imports no renderer: // (CLAUDE.md rule 3). This file is the stream, the round and the rules. It imports no renderer:
// render.js reads DRILL_COLS/PIP_FACES and draws. // render.js reads DRILL_COLS and draws.
import { TUNE } from './config.js'; import { TUNE } from './config.js';
import { G } from './state.js'; import { G } from './state.js';
import { zombies, spawnZombie } from './zombies.js'; import { zombies, spawnZombie } from './zombies.js';
import { voice, blip, sfx } from './audio.js'; import { voice, blip, sfx } from './audio.js';
// The bib colours. Six, and every entry carries `pips` as well as a hue: roughly 8% of men cannot // The bib colours. Six, and a colour is on court at most once, so a colour always names exactly one
// separate the red from the green, and a mode whose whole input is "which colour is that" is // body. `tone` is its PITCH — see the arpeggio in newRound. The recorded umpire reading the list
// unplayable for them on hue alone. // aloud is deleted (Oliver: "daniel announcing is maximum annoying"): at ~0.8 s a word he needed
// `tone` is the colour's PITCH — see the arpeggio in newRound. **The recorded umpire reading the // four seconds to get through a list of four, and with rounds running continuously that is a man
// list aloud is DELETED**: at ~0.8 s a word he took four seconds to get through a list of four, and // talking over the entire mode. A rising figure says the same thing in half a second and does not
// with rounds running continuously that is a man talking over the entire mode. Oliver: "daniel // wear out. (`drill_*.m4a` stay in assets — cheap to keep, and a slower variant could want them.)
// announcing is maximum annoying". A rising four-note figure says the same thing in half a second //
// and does not wear out. (`drill_*.m4a` stay in assets — cheap to keep, and the call could come // THERE IS NO REDUNDANT CHANNEL ANY MORE, and that is a known accessibility gap rather than an
// back for a slower variant of the mode.) // oversight. Die-face dots rode on the bib and the HUD chip so the mode could be played without
// separating red from green (~8% of men cannot) — but they were noise to everyone who can, and they
// shrank to nothing on a body at distance, which is exactly where the channel had to work. Cut on
// Oliver's call. If the mode ships, the answer is bibs that differ by SHAPE or marking at sprite
// scale, not dots: a channel that fails at the far baseline is not a channel.
export const DRILL_COLS = [ export const DRILL_COLS = [
{ key: 'red', col: '#e5342c', ink: '#2a0806', pips: 1, tone: 392, name: 'RED' }, { key: 'red', col: '#e5342c', tone: 392, name: 'RED' },
{ key: 'blue', col: '#2f6bff', ink: '#050f2c', pips: 2, tone: 440, name: 'BLUE' }, { key: 'blue', col: '#2f6bff', tone: 440, name: 'BLUE' },
{ key: 'yellow', col: '#f2c318', ink: '#2e2202', pips: 3, tone: 494, name: 'YELLOW' }, { key: 'yellow', col: '#f2c318', tone: 494, name: 'YELLOW' },
{ key: 'green', col: '#27bd4a', ink: '#04240f', pips: 4, tone: 587, name: 'GREEN' }, { key: 'green', col: '#27bd4a', tone: 587, name: 'GREEN' },
{ key: 'purple', col: '#9b3fd8', ink: '#1c0530', pips: 5, tone: 659, name: 'PURPLE' }, { key: 'purple', col: '#9b3fd8', tone: 659, name: 'PURPLE' },
{ key: 'white', col: '#f2f0e8', ink: '#20201c', pips: 6, tone: 784, name: 'WHITE' }, { key: 'white', col: '#f2f0e8', tone: 784, name: 'WHITE' },
];
// The redundant channel is drawn as a DIE FACE, not a row of dots: five dots have to be counted, a
// die is recognised — and a redundant channel is pointless if reading it costs more than the hue.
// Unit coords in a -1..1 box, scaled onto the bib and the HUD chip by the same table so the two
// readouts cannot drift.
export const PIP_FACES = [
[[0, 0]],
[[-0.6, -0.6], [0.6, 0.6]],
[[-0.65, -0.65], [0, 0], [0.65, 0.65]],
[[-0.6, -0.6], [0.6, -0.6], [-0.6, 0.6], [0.6, 0.6]],
[[-0.7, -0.7], [0.7, -0.7], [0, 0], [-0.7, 0.7], [0.7, 0.7]],
[[-0.7, -0.7], [0.7, -0.7], [-0.7, 0], [0.7, 0], [-0.7, 0.7], [0.7, 0.7]],
]; ];
// ---- THE RAMP. One number, 0..1 off time survived; everything else is a lerp on it. The stream is // ---- THE RAMP. One number, 0..1 off time survived; everything else is a lerp on it. The stream is
+9 -1
View File
@@ -1279,7 +1279,15 @@ export function update(dt) {
const chasing = ball.lastHit === 'O' || (G.mode === 'splatter' && !G.interlude && ball.vz > 0); const chasing = ball.lastHit === 'O' || (G.mode === 'splatter' && !G.interlude && ball.vz > 0);
// no `slow` here: tapping must not freeze your legs, or anticipating a shot loses it // no `slow` here: tapping must not freeze your legs, or anticipating a shot loses it
const rate = TUNE.playerSpeed * TUNE.autoAssist * dt * (chasing ? 1 : 0.55); const rate = TUNE.playerSpeed * TUNE.autoAssist * dt * (chasing ? 1 : 0.55);
const tgt = chasing ? intercept('P') : HOME.P; // run to the ball, then recover home // Run to the ball — then recover in DEPTH ONLY, holding whatever lane you are standing in.
// Recovering to HOME.P's x undid the sidestep: measured, a step to 1.94 m was dragged back to
// 0.77 m over one ball's flight, so firing gave back most of a dodge you had spent a cooldown
// on. That is the "welded in place" complaint the sidestep exists to answer, arriving slowly
// instead of all at once. TENNIS keeps the full recovery — recovering to centre is the real
// sport's footwork and there is no sidestep there to protect.
const tgt = chasing ? intercept('P')
: G.mode === 'splatter' ? { x: P.x, z: HOME.P.z }
: HOME.P;
const dx = tgt.x - P.x, dz = tgt.z - P.z, d = Math.hypot(dx, dz); const dx = tgt.x - P.x, dz = tgt.z - P.z, d = Math.hypot(dx, dz);
if (d > 0.35) { // deadzone: stand ready near the spot instead of dithering over it if (d > 0.35) { // deadzone: stand ready near the spot instead of dithering over it
P.x += dx / d * Math.min(rate, d); P.x += dx / d * Math.min(rate, d);
+1 -12
View File
@@ -5,7 +5,7 @@ import { TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, COURT, P_BOUNDS, P
import { P, O, ball, score, crowd, G, court, tour } from './state.js'; import { P, O, ball, score, crowd, G, court, tour } from './state.js';
import { predictLanding, previewServe, slotId, slotStock } from './gameplay.js'; import { predictLanding, previewServe, slotId, slotStock } from './gameplay.js';
import { zombies, gore, piles, parts, pellets, extras, shots, pops } from './zombies.js'; import { zombies, gore, piles, parts, pellets, extras, shots, pops } from './zombies.js';
import { DRILL_COLS, PIP_FACES } from './drill.js'; import { DRILL_COLS } from './drill.js';
// ---------------------------------------------------------------- palette ---- // ---------------------------------------------------------------- palette ----
// tennis palette -> drained splatter palette; the transition lerps between them. // tennis palette -> drained splatter palette; the transition lerps between them.
@@ -707,16 +707,6 @@ function drawBib(zb, hMeters, bob) {
ctx.closePath(); ctx.fill(); ctx.closePath(); ctx.fill();
ctx.fillStyle = 'rgba(0,0,0,0.30)'; // a seam, so it sits ON the body ctx.fillStyle = 'rgba(0,0,0,0.30)'; // a seam, so it sits ON the body
ctx.fillRect(cx - wTop, Math.round(y0), wTop * 2, Math.max(1, h * 0.012)); ctx.fillRect(cx - wTop, Math.round(y0), wTop * 2, Math.max(1, h * 0.012));
pips(C, cx, (y0 + y1) / 2, Math.max(0.8, h * 0.024), h * 0.055);
}
// The redundant channel, as a DIE FACE (PIP_FACES). A row of five dots has to be counted; a die is
// recognised. Shared by the bib and the HUD chip so the two can never disagree.
function pips(C, cx, cy, r, spread) {
ctx.fillStyle = C.ink;
for (const [ux, uy] of PIP_FACES[C.pips - 1]) {
ctx.beginPath(); ctx.arc(cx + ux * spread, cy + uy * spread, r, 0, 7); ctx.fill();
}
} }
// THE CALLED LIST, on screen. Oliver's call: the umpire reads it aloud AND it shows as a pattern — // THE CALLED LIST, on screen. Oliver's call: the umpire reads it aloud AND it shows as a pattern —
@@ -734,7 +724,6 @@ function drawDrillHud() {
ctx.globalAlpha = done ? 0.28 : 1; ctx.globalAlpha = done ? 0.28 : 1;
ctx.fillStyle = 'rgba(6,4,6,0.8)'; ctx.fillRect(x - 1, y - 1, w + 2, h + 2); ctx.fillStyle = 'rgba(6,4,6,0.8)'; ctx.fillRect(x - 1, y - 1, w + 2, h + 2);
ctx.fillStyle = C.col; ctx.fillRect(x, y, w, h); ctx.fillStyle = C.col; ctx.fillRect(x, y, w, h);
pips(C, x + w / 2, y + h / 2, 2.2, 6); // the same die face as the bib
if (i === d.step) { // the one you owe them right now if (i === d.step) { // the one you owe them right now
ctx.strokeStyle = UI.cream; ctx.lineWidth = 2; ctx.strokeStyle = UI.cream; ctx.lineWidth = 2;
ctx.strokeRect(x - 2, y - 2, w + 4, h + 4); ctx.strokeRect(x - 2, y - 2, w + 4, h + 4);