From 35671c689b017092d9f0e9888a1cabddd7970a01 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Thu, 13 Aug 2026 19:08:18 +0200 Subject: [PATCH] A facing flip must be earned: the racket stops flashing across the body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oliver, women's tour: 'the racket she is holding flashing left and right very fast' during auto-run. The rolled cycles carry a real racket, and the mirror flip teleports it across the body — the per-frame steering flutter the old racketless art could hide. earnFlip() requires an auto-steered direction to persist TUNE.faceFlip (0.13 s) before the body mirrors, at all three steering sites (player assist, tennis CPU, match opponent); manual input stays instant. Measured over an 8 s live rally: 4 player flips, 2 opponent flips — every one an actual turn. Co-Authored-By: Claude Fable 5 --- index.html | 2 +- src/config.js | 3 +++ src/gameplay.js | 19 +++++++++++++++---- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index 19b430f..cf3c19c 100644 --- a/index.html +++ b/index.html @@ -431,6 +431,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/config.js b/src/config.js index 435c3f2..793b8e3 100644 --- a/src/config.js +++ b/src/config.js @@ -43,6 +43,9 @@ export const TUNE = { // faults, which is what makes tap = safe second serve a real decision runHz: 2.4, // stride cycles/s for the rolled 6/8-frame run sets. The old hardcoded 4.5 was // tuned on 2-4 near-duplicate frames; on eight real phases it strobed + faceFlip: 0.13, // s an auto-steered direction must persist before the body mirrors (earnFlip). + // The rolled cycles hold a real racket, and an un-earned flip teleports it + // across the body — the "racket flashing left and right" report serveSpin: 1.3, // extra gravity a full-power serve carries (was a fixed 0.35). Without it the // net-clearance loop ate the whole smash: a flat fast ball can't get over the // tape AND come down on a target, so it got throttled back to walking pace. diff --git a/src/gameplay.js b/src/gameplay.js index 63cd657..172cf2e 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -1115,7 +1115,7 @@ function racketAI(rz, dt) { // facing RIGHT and the bodies are drawn facing LEFT). This line wrote a field nothing reads for a // zombie, so the match opponent's turn was left to the generic sticky-face line in updateZombies. // vx is kept so a body that is knocked down mid-chase can carry the run into its collapse. - if (Math.abs(dx) > 0.2) rz.face = dx > 0 ? 1 : -1; + earnFlip(rz, 'face', Math.abs(dx) > 0.2 ? (dx > 0 ? 1 : -1) : 0, dt); rz.vx = dx / d * step / dt; } else { rz.vx = 0; if (rz.pose === 'run') rz.pose = 'idle'; } if (rz.swingCool <= 0 && rz.swingT < 0 @@ -1309,6 +1309,17 @@ function expirePose(ch, dt) { if (ch.pose === 'swing' || ch.pose === 'serve') { ch.poseT += dt; if (ch.poseT > POSE_TIME) ch.pose = 'idle'; } } +// A FACING FLIP MUST BE EARNED. The rolled run cycles carry a real racket, and a mirror flip +// teleports it across the body — so the per-frame steering flutter the old racketless art could +// hide reads as "the racket she is holding flashing left and right very fast" (Oliver, women's +// tour, where the drawn racket is biggest). A flip now needs the wanted direction to PERSIST for +// TUNE.faceFlip seconds. Every auto-steered body routes through here (the player's assist, the +// tennis CPU, the match opponent); MANUAL input stays instant — a thumb reversal is intent. +function earnFlip(ch, field, want, dt) { + if (!want || want === ch[field]) { ch.flipT = 0; return; } + if ((ch.flipT = (ch.flipT || 0) + dt) >= TUNE.faceFlip) { ch[field] = want; ch.flipT = 0; } +} + // ------------------------------------------------------------- ball boys ---- // The fetch boy is unchanged: sprint the width of the court, scoop the dead ball in passing. function fetchRound(b, dt) { @@ -1522,8 +1533,8 @@ export function update(dt) { // run animation" whether he was moving or not. The pose follows MOVEMENT in every mode now, so // auto-fire uses the ordinary run animation and standing still is standing still. if (P.pose !== 'swing' && P.pose !== 'serve') P.pose = moving ? 'run' : 'idle'; - if (mx) P.facing = mx > 0 ? 1 : -1; - else if (Math.abs(assist) > 0.25) P.facing = assist > 0 ? 1 : -1; // hysteresis: small corrections don't flip facing + if (mx) { P.facing = mx > 0 ? 1 : -1; P.flipT = 0; } // manual: instant, and it resets the earn clock + else earnFlip(P, 'facing', Math.abs(assist) > 0.25 ? (assist > 0 ? 1 : -1) : 0, dt); } // ...and out of 'rally' he STANDS. 'run' is the one pose nothing else expires (expirePose only // handles swing/serve), and P.runT stops advancing outside the block, so a beat that began @@ -1616,7 +1627,7 @@ export function update(dt) { O.x += dx / d * Math.min(spd * dt, d); O.z += dz / d * Math.min(spd * dt, d); O.pose = O.pose === 'swing' ? 'swing' : 'run'; O.runT += dt; - if (Math.abs(dx) > 0.25) O.facing = dx > 0 ? 1 : -1; + earnFlip(O, 'facing', Math.abs(dx) > 0.25 ? (dx > 0 ? 1 : -1) : 0, dt); } else if (O.pose === 'run') O.pose = 'idle'; G.aiSwingCool -= dt; if (incoming && G.aiSwingCool <= 0 && O.swingT < 0) {