diff --git a/PROJECT.md b/PROJECT.md index 9d4e960..9e359cb 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -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 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. +- **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 (`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 @@ -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 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:** 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 diff --git a/index.html b/index.html index 19dd183..5858af0 100644 --- a/index.html +++ b/index.html @@ -399,6 +399,6 @@ - + diff --git a/src/drill.js b/src/drill.js index b56a5ae..ec96829 100644 --- a/src/drill.js +++ b/src/drill.js @@ -16,41 +16,33 @@ // // 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: -// render.js reads DRILL_COLS/PIP_FACES and draws. +// render.js reads DRILL_COLS and draws. import { TUNE } from './config.js'; import { G } from './state.js'; import { zombies, spawnZombie } from './zombies.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 -// separate the red from the green, and a mode whose whole input is "which colour is that" is -// unplayable for them on hue alone. -// `tone` is the colour's PITCH — see the arpeggio in newRound. **The recorded umpire reading the -// list aloud is DELETED**: at ~0.8 s a word he took four seconds to get through a list of four, and -// with rounds running continuously that is a man talking over the entire mode. Oliver: "daniel -// 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 -// back for a slower variant of the mode.) +// The bib colours. Six, and a colour is on court at most once, so a colour always names exactly one +// body. `tone` is its PITCH — see the arpeggio in newRound. The recorded umpire reading the list +// aloud is deleted (Oliver: "daniel announcing is maximum annoying"): at ~0.8 s a word he needed +// four seconds to get through a list of four, and with rounds running continuously that is a man +// talking over the entire mode. A rising figure says the same thing in half a second and does not +// wear out. (`drill_*.m4a` stay in assets — cheap to keep, and a slower variant could want them.) +// +// THERE IS NO REDUNDANT CHANNEL ANY MORE, and that is a known accessibility gap rather than an +// 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 = [ - { key: 'red', col: '#e5342c', ink: '#2a0806', pips: 1, tone: 392, name: 'RED' }, - { key: 'blue', col: '#2f6bff', ink: '#050f2c', pips: 2, tone: 440, name: 'BLUE' }, - { key: 'yellow', col: '#f2c318', ink: '#2e2202', pips: 3, tone: 494, name: 'YELLOW' }, - { key: 'green', col: '#27bd4a', ink: '#04240f', pips: 4, tone: 587, name: 'GREEN' }, - { key: 'purple', col: '#9b3fd8', ink: '#1c0530', pips: 5, tone: 659, name: 'PURPLE' }, - { key: 'white', col: '#f2f0e8', ink: '#20201c', pips: 6, 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]], + { key: 'red', col: '#e5342c', tone: 392, name: 'RED' }, + { key: 'blue', col: '#2f6bff', tone: 440, name: 'BLUE' }, + { key: 'yellow', col: '#f2c318', tone: 494, name: 'YELLOW' }, + { key: 'green', col: '#27bd4a', tone: 587, name: 'GREEN' }, + { key: 'purple', col: '#9b3fd8', tone: 659, name: 'PURPLE' }, + { key: 'white', col: '#f2f0e8', tone: 784, name: 'WHITE' }, ]; // ---- THE RAMP. One number, 0..1 off time survived; everything else is a lerp on it. The stream is diff --git a/src/gameplay.js b/src/gameplay.js index 1bb23d3..f9467ae 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -1279,7 +1279,15 @@ export function update(dt) { 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 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); if (d > 0.35) { // deadzone: stand ready near the spot instead of dithering over it P.x += dx / d * Math.min(rate, d); diff --git a/src/render.js b/src/render.js index abc13e8..0cd9783 100644 --- a/src/render.js +++ b/src/render.js @@ -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 { predictLanding, previewServe, slotId, slotStock } from './gameplay.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 ---- // tennis palette -> drained splatter palette; the transition lerps between them. @@ -707,16 +707,6 @@ function drawBib(zb, hMeters, bob) { ctx.closePath(); ctx.fill(); 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)); - 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 — @@ -734,7 +724,6 @@ function drawDrillHud() { 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 = 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 ctx.strokeStyle = UI.cream; ctx.lineWidth = 2; ctx.strokeRect(x - 2, y - 2, w + 4, h + 4);