From 90d7ac1096e06148e589c35d201226bdc7f9f201 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Fri, 14 Aug 2026 14:39:36 +0200 Subject: [PATCH] The bench learns to host the living: two player slots for run-cycle tuning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oliver: "add a normal player male and female to the hidden bench and let it run there." The fam walk (up/down) now runs two slots past ZFAMS — PLAYER MALE and PLAYER FEMALE. In a living slot the racket zombie steps outside the frame, G.hideO lifts (splatter set it when the match turned), and the real O runs the walk stop's own back-and-forth at the classic CPU's speed class, so the cadence on the bench is the cadence the game plays. SK() reads the slot for the _f suffix, so both tours review without touching the picked tour. The HUD swaps the wound readout for the tuning one: runHz and aiSpeed, the dials a live session turns through T.TUNE. T also gains a keyPressed alias. Co-Authored-By: Claude Fable 5 --- index.html | 2 +- src/beats.js | 26 +++++++++++++++++++++++--- src/main.js | 2 +- src/render.js | 19 +++++++++++++++++-- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index 963e156..0a9f01c 100644 --- a/index.html +++ b/index.html @@ -494,6 +494,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/beats.js b/src/beats.js index 3ede5e8..3eef50b 100644 --- a/src/beats.js +++ b/src/beats.js @@ -199,6 +199,8 @@ export function endBench() { // wound stage — the bench must ask the same question the game asks or it is testing something else. function benchApply() { const zb = zombies.find(z => z.variant === 'r'); if (!zb) return; + // the two slots past ZFAMS are the LIVING opponents; render's SK() reads b.char for the suffix + G.bench.char = G.bench.fam >= ZFAMS.length ? (G.bench.fam - ZFAMS.length ? 'of' : 'o') : null; zb.hpMax = BENCH_HP; zb.hp = Math.max(1, Math.round(BENCH_HP * [1, 0.5, 0.2][G.bench.stage])); // ...and the named states take the wound stage with them: z_r at full health, @@ -228,9 +230,10 @@ export function benchKey(code) { const b = G.bench; if (code === 'ArrowLeft' || code === 'KeyA') b.stage = (b.stage + 2) % 3; else if (code === 'ArrowRight' || code === 'KeyD') b.stage = (b.stage + 1) % 3; - // up/down walks the OPPONENT FAMILIES — one today, one row in config.ZFAMS per variant after that - else if (code === 'ArrowUp' || code === 'KeyW') b.fam = (b.fam + ZFAMS.length - 1) % ZFAMS.length; - else if (code === 'ArrowDown' || code === 'KeyS') b.fam = (b.fam + 1) % ZFAMS.length; + // up/down walks the OPPONENT FAMILIES — plus two LIVING slots past the end (the tennis + // opponents m/f, Oliver's run-cycle finetune bench): the zombie steps aside and the real O runs + else if (code === 'ArrowUp' || code === 'KeyW') b.fam = (b.fam + ZFAMS.length + 1) % (ZFAMS.length + 2); + else if (code === 'ArrowDown' || code === 'KeyS') b.fam = (b.fam + 1) % (ZFAMS.length + 2); // F plays the fall. Before the fallback below, which treats ANY other key as "next state" — a new // control that forgets to land above that line silently steps the stop instead. else if (code === 'KeyF') return benchFall(); @@ -247,6 +250,23 @@ export function updateBench(dt) { // ...and a body that is COLLAPSING is not pacing. tickMatchFall owns the frame while the fall runs, // so the walk stop must stop walking or the corpse slides sideways through its own death. if (G.matchFall) return; + // THE LIVING SLOTS: the zombie waits outside the frame while the real opponent runs the same + // back-and-forth the walk stop uses — same speed class as the classic CPU, so the cadence being + // judged is the cadence the game plays at. Every stop shows the run; the run is what the bench + // slot exists to tune. + if (b.char) { + zb.x = 40; zb.vx = 0; zb.pose = 'idle'; + G.hideO = false; // splatter hid him when the match turned; the bench un-hides + O.z = 5.5; + const step = TUNE.aiSpeed * 0.7 * dt; + O.x += b.dir * step; + O.runT += dt; + if (Math.abs(O.x) > 4.0) { b.dir *= -1; O.x = Math.sign(O.x) * 4.0; } + O.facing = b.dir; + O.pose = 'run'; + return; + } + O.pose = 'idle'; O.x = HOME.O.x; O.z = HOME.O.z; G.hideO = true; // fam stops: the living man steps back out if (item.walk) { // THE STRIDE STEPS OFF GROUND COVERED, like the real one — a bench that animated on a clock // would be flattering exactly the sprites that foot-slide in play. diff --git a/src/main.js b/src/main.js index dcfe9ca..a4b2ede 100644 --- a/src/main.js +++ b/src/main.js @@ -556,6 +556,6 @@ if (location.hash === '#bench') setTimeout(() => { start(null); intoSplatter(); requestAnimationFrame(frame); // exposed for in-browser sanity checks and simulated playtests -window.T = { TUNE, ball, P, O, score, G, zombies, laserScene, laserFire, pause: v => { paused = v !== false; }, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, spawnBarrel, grantWeapon, toggleTestMode, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer, splatPoint, solveShot, inServeBox, inCourt, proj, setupPoint, forceZombies, deliverTin, selectWeapon, showModal, dismissModal, dragServe, startDuel, resolveInterlude, flickTarget, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, +window.T = { TUNE, ball, P, O, score, G, zombies, keyPressed, laserScene, laserFire, pause: v => { paused = v !== false; }, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, spawnBarrel, grantWeapon, toggleTestMode, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer, splatPoint, solveShot, inServeBox, inCourt, proj, setupPoint, forceZombies, deliverTin, selectWeapon, showModal, dismissModal, dragServe, startDuel, resolveInterlude, flickTarget, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, stats, step: dt => update(dt), render, press: keyPressed, FALL, benchFall, get phase() { return G.phase; }, set phase(v) { G.phase = v; } }; diff --git a/src/render.js b/src/render.js index 8648c04..fe5dea2 100644 --- a/src/render.js +++ b/src/render.js @@ -954,8 +954,18 @@ function drawBenchHud() { const y = CARD_TOP + 4; ctx.textAlign = 'center'; ctx.font = B(20); - shadowText((ZFAMS.length > 1 ? ZFAMS[b.fam].toUpperCase() + ' · ' : '') + BENCH[b.i].name, + shadowText(b.char + ? 'PLAYER ' + (b.char === 'of' ? 'FEMALE' : 'MALE') + ' · RUN CYCLE' + : (ZFAMS.length > 1 ? ZFAMS[b.fam].toUpperCase() + ' · ' : '') + BENCH[b.i].name, W / 2, y, UI.cream); + if (b.char) { // the finetune readout: the dials a live tuning session actually turns + ctx.font = F(8); + shadowText('runHz ' + TUNE.runHz + ' · aiSpeed ' + TUNE.aiSpeed + + ' · T.TUNE.runHz = … to tune live', W / 2, y + 24, UI.warm); + ctx.font = F(7, 0); + shadowText('↑/↓: OPPONENT · THE LIVING SLOTS SHOW ONLY THE RUN', W / 2, H - 26, UI.dim); + return; + } ctx.font = F(8); // ...and say so when the stop has no wound variant. The serve and the fall exist for the HEALTHY // body only so far, so a stage label there would be claiming art that has not been drawn — and the @@ -1438,7 +1448,12 @@ function drawPiles(near) { // split at the net like zombies so height occludes c // one whose PNG has not loaded yet — falls straight through to the men's frame. That fallback is // deliberate: a cached 404 on one female frame degrades to a man for a frame, it does not blink the // body out of existence (the same rule drawZombie's missing-frame guard is built on). -const SK = n => (tour.sfx && SPR[n + tour.sfx]) ? n + tour.sfx : n; +const SK = n => { + // the bench's living slots pick the suffix directly (G.bench.char 'o'/'of'), so both tennis + // opponents can be reviewed without touching the picked tour + const sfx = G.bench && G.bench.char != null ? (G.bench.char === 'of' ? '_f' : '') : tour.sfx; + return (sfx && SPR[n + sfx]) ? n + sfx : n; +}; // BOUNCE_IDLE.dx/apex are measured off the DRAWN PALM (see config), so a tour whose player reaches // differently needs its own pair. Only the women's row carries them; ?? falls back to the men's.