diff --git a/HANDOFF.md b/HANDOFF.md index 30af9e2..ceed4a9 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -155,6 +155,33 @@ New foundations you must know before touching anything: exists, but only in tennis mode where there is nothing on the grass but a ball. `BOY_H` 1.2 → 1.5. - `TUNE.burnSpread` is a T-panel knob if Oliver wants fire hotter still. +## Second feedback round — "do it properly" (2026-07-27) +Oliver rejected the procedural fakes on sight. The rule this round: **animation gets drawn frames.** +- **Every brute wound stage now has a 2-frame lurch** (`z_gw`/`z_gwb`, `z_gw2`/`z_gw2b`, …). The + procedural squash-and-sway that replaced them read as fake immediately — "that's where the comedy + comes from". Generated by handing the generator a composite of the four A-frames + (`assets/_sheet_gwound_a.png`, built with PIL) and asking for the stride swapped. + **Gotcha:** the generator gave every B frame a dragged pole, but only stage 1's A frame has one — + it would have blinked in and out each step. Stripped by keeping the largest connected blob + (stages 2–3) and, where the pole touched the body (stage 4), by cutting on column density: the + torso is dense, a dragged pole is a thin diagonal of ~7 px per column. +- **Frames step off DISTANCE COVERED, never elapsed time** — `boy.dist` for ball boys, + `zb.z / TUNE.bruteStride` for brutes. Time-driven cycles moonwalk whenever speed and frame rate + disagree, which is exactly what "he slides faster than the animation suggests" was. + Height for a wound stage ALWAYS comes from the A frame or the sprite pops size every step. +- **One boy, one slot.** `sendBoy` no longer replaces a boy who is already out — that was the + "appears and disappears": every kill teleported him back to the wing, so a kill streak restarted + him several times a second. New work is appended to his round (`jobs`). One crossing is now 10.5s. +- **The applauding crowd is a real two-frame animation** (`x_crowd` seated / `x_crowd2` arms up), + swapped per column on its own phase. The previous "smooth wave" still shifted each column by a + FRACTIONAL dy, and nearest-neighbour resampling of a scaled image at a fractional y re-snaps its + pixel rows every frame — that boiling beside the HUD was the "scoreboard flicker". The board art + is 99.9% opaque, so nothing was ever showing *through* it. **Test:** render twice without + stepping — identical pixels means nothing in the draw path rolls dice; and with the crowd calm + the frame-to-frame pixel delta must be 0.0%. +- Editing a full-scene image (`x_crowd` → `x_crowd2`) preserved the layout far better than expected: + mean difference 3.2/255, railing and tier rows unmoved. Worth reusing for other 2-frame swaps. + ## Gotchas learned the hard way - Browser-pane verification: python http.server sends no cache headers, so edited ES modules often load STALE even after location.reload(). Reliable fix: preview_stop + diff --git a/assets/_sheet_gwound_a.png b/assets/_sheet_gwound_a.png new file mode 100644 index 0000000..3ce82c6 Binary files /dev/null and b/assets/_sheet_gwound_a.png differ diff --git a/assets/_sheet_gwound_b.png b/assets/_sheet_gwound_b.png new file mode 100644 index 0000000..d776899 Binary files /dev/null and b/assets/_sheet_gwound_b.png differ diff --git a/assets/x_crowd2.png b/assets/x_crowd2.png new file mode 100644 index 0000000..2ad76d3 Binary files /dev/null and b/assets/x_crowd2.png differ diff --git a/assets/z_gw2b.png b/assets/z_gw2b.png new file mode 100644 index 0000000..c28f3ce Binary files /dev/null and b/assets/z_gw2b.png differ diff --git a/assets/z_gw3b.png b/assets/z_gw3b.png new file mode 100644 index 0000000..4f9e76f Binary files /dev/null and b/assets/z_gw3b.png differ diff --git a/assets/z_gw4b.png b/assets/z_gw4b.png new file mode 100644 index 0000000..0733592 Binary files /dev/null and b/assets/z_gw4b.png differ diff --git a/assets/z_gwb.png b/assets/z_gwb.png new file mode 100644 index 0000000..dda1a38 Binary files /dev/null and b/assets/z_gwb.png differ diff --git a/config.js b/config.js index cf54b50..4415b5b 100644 --- a/config.js +++ b/config.js @@ -31,8 +31,9 @@ export const TUNE = { zMax: 3, // shamblers on court in game 1 (cap grows +1 per game to zMaxCap; 0 = spawns off, for tests) zMaxCap: 14, // absolute shambler cap changeoverT: 5.0, // s the changeover breather lasts (after odd games, as the rules demand) - boyMopSpeed: 5.0, // m/s a MOPPING ball boy crosses at (a fetching one still sprints at 7.5). - // Sized so one crossing ~fills the changeover instead of stranding him mid-court. + boyMopSpeed: 2.2, // m/s a MOPPING ball boy crosses at (a fetching one still sprints at 7.5) + boyStride: 0.28, // m of travel per animation frame. Frames are driven by DISTANCE, not time, so + // the feet cannot slide: change a boy's speed and his legs keep up on their own. gameBeatT: 1.6, // s the game-end beat lasts on even (non-changeover) games eatenR: 0.9, // m a shambler must close to eat you — matches the visual sprite overlap zSnap: 0.9, // m of lateral slack at zero range — the aim cone's base width (fat-thumb forgiveness) @@ -92,6 +93,8 @@ export const TUNE = { // full pierce simply killed the whole lane outright and nothing was left to catch fire. burnPanic: 1.0, // walk-speed multiplier while burning (1 = off; crank it for panicked sprinting) setCardT: 2.0, // s the centered SET card holds (the 'kurze unterbrechung' before the horde) + bruteStride: 0.5, // m the brute covers per lurch frame — it is the slowest thing on court, so + // its cycle is driven by ground covered rather than by the clock bruteHp: 5, // splat-quality hits (or pellets) a brute ('g') soaks before it dies — one wound skin per stage shieldDown: 1.5, // s a shield zombie's ('s') chair stays dropped after a block — the kill window meleeRange: 1.7, // m — a racket swish cleaves shamblers this close to you @@ -121,6 +124,9 @@ COURT.svcNear = COURT.netZ + 6.4; // near service line // world sizes (meters) export const PLAYER_H = 1.88; export const BOY_H = 1.5; // was 1.2 — the mopping boy read as a distant speck at that scale +// how far off-court a boy starts. The mop boy comes in from just off-screen (he is slow, and +// waiting 8s for him to walk into shot is dead time); a fetching boy sprints in from further out. +export const BOY_ENTER = { mop: 9.5, run: 13 }; export const UMP_H = 3.0; export const UMP_X = 5.6; // chair right beside the doubles line — x 7.1 projected off-screen in portrait diff --git a/gameplay.js b/gameplay.js index 8851bfd..b16ff13 100644 --- a/gameplay.js +++ b/gameplay.js @@ -931,14 +931,16 @@ export function update(dt) { // ball boy: sprint across, scoop the ball in passing, despawn off the far side const boy = G.boy; if (boy) { - // mopping is not sprinting: the mop crossing takes ~6.6s, which is what fills the changeover - boy.x += boy.dir * (boy.mop ? TUNE.boyMopSpeed : 7.5) * dt; boy.runT += dt; + // mopping is not sprinting. `dist` is what drives his animation frames (render.drawBoy), so + // the legs are tied to the ground he covers and can never out-run or lag behind the walk. + const step = boy.dir * (boy.mop ? TUNE.boyMopSpeed : 7.5) * dt; + boy.x += step; boy.runT += dt; boy.dist = (boy.dist || 0) + Math.abs(step); if (!boy.mop && !ball.collected && !ball.active) { // a mop boy has no ball to chase — he must hold his lane and clean it const dz = ball.z - boy.z; boy.z += Math.sign(dz) * Math.min(Math.abs(dz), 3 * dt); // track the rolling ball a little if (Math.abs(boy.x - ball.x) < 0.4 && Math.abs(dz) < 1.2) ball.collected = true; } - if (Math.abs(boy.x) > 13.5) G.boy = null; + if (Math.abs(boy.x) > 13.5) G.boy = null; // off the far wing — his round is done } if (G.outMarker && (G.outMarker.t -= dt) <= 0) G.outMarker = null; diff --git a/index.html b/index.html index d80b8ee..bd3c06f 100644 --- a/index.html +++ b/index.html @@ -60,6 +60,6 @@
TAP TO START
- +