Stop back-dating flicks, and give the half-volley rescue the right deadline

Oliver's v367 log: every 62-64 ms flick landed 76-92 ms early and a 97 ms
one whiffed — v366's touch-down timing for flicks was wrong; a flick is
timed by its swipe (flickLead now 0, the age is still logged). The
early-swing half-volley rescue checked the bounce against the long volley
window and then expired on the bounce frame; it now uses the shorter
groundstroke window. The log also records where he stood when the
opponent struck and where the auto-run was taking him on a miss. v368.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5.5 committed 2026-10-14 13:13:08 +02:00
1 parent dfdb699f9d
commit fe090f2fba
5 files changed
+37 -11

No files matched your search

+19
View File
@@ -5466,6 +5466,25 @@ in the same test.
The swing log's HIT line now also says which way the timing missed: `late 40ms` (the ball was already
there when the window opened) or `early 40ms`. That was the question his first loss could not answer.
### His v367 log: the flick back-dating was wrong, and the half-volley rescue used the wrong window (v368)
- **v366's flick back-dating made slow flicks EARLY.** Every 62-64 ms flick landed 76-92 ms early (q
0.05-0.2). A 97 ms flick whiffed "14 ms after the window closed" and cost a point. The 31-45 ms
flicks were fine. A flick is timed by its SWIPE, not by its touch-down. `TUNE.flickLead` is now 0
(knob kept). The touch's age is still logged for both gestures, and taps keep their touch-down timing.
- **The early-swing half-volley rescue judged its deadline against the swing's current, VOLLEY-length
window.** After the bounce the shorter groundstroke window applies, so it decided to wait and then
expired on the bounce frame. That is his 8.98 s "waited for the bounce": the Brute had come in and
dug his drop out weakly (q 0.3, 15 m/s), so it was a sitter, lost. The deadline is now the
post-bounce window, less a frame.
- **Point 3 (a 32 m/s topspin return of his serve into the left corner):** he ended up 1.5 m FORWARD and
only 1 m left. Rebuilt headless twice (from the baseline, and after his own serve); the auto-run
reached the ball both times (5-15 cm). Not reproduced. THEM lines now carry where the player stood
(`px,pz`) and MISS lines where the auto-run was taking him (`runTo`), so the next one explains itself.
- Observed, not changed: the CPU's "heavy topspin" carries its extra gravity THROUGH the bounce, so it
skids low afterwards and nearly every contact in his logs is 0-25 cm off the grass. Real topspin
kicks UP. Resetting `ball.g` at the bounce would fix the picture but moves every timing in the
rally (all four planners), so it is a decision to put to him.
# OPEN — leftovers, in rough priority order
Nothing here is broken; these are known gaps and unverified risks.
+1 -1
View File
@@ -1140,6 +1140,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
</div>
</div>
<span id="recLamp" hidden>REC</span>
<script type="module" src="src/main.js?v=367"></script>
<script type="module" src="src/main.js?v=368"></script>
</body>
</html>
+2 -1
View File
@@ -647,7 +647,8 @@ export const TUNE = {
// the swing window, at any pace. 0 = the old distance ring (matchRingSweet/Band)
ringBand: 0.35, // share of the half-window either side of the sweet moment that draws gold (edge = q 0.65)
ringLead: 0.9, // s before contact the timed ring appears
flickLead: 0.1, // s: a flick is timed from touch-down too, up to this much (a slow drag keeps a window)
flickLead: 0, // s a flick's swing is back-dated toward touch-down (v366 used 0.1 — his next log showed
// every 62-64 ms flick landing ~85 ms EARLY: a flick is timed by its swipe, not its touch)
// ---- THE READ (2026-08-28, Oliver: past the HEAD COACH "the opponents are very fast and barely
// make mistakes any more — we need a twist"). The dead staff LEARN YOUR PATTERNS: land the same
// lane readHabit times in a row and the opponent has the habit — an EXPECTED ball is anticipated
+11 -4
View File
@@ -378,7 +378,7 @@ function doHit(ch, quality, aim) {
const kind = soft === null ? (aim && aim.az < 0 ? 'deep' : 'drive') + (ball.spin > 1 ? ' topspin' : ball.spin < 1 ? ' slice' : '')
: soft === 0 ? 'drop' : soft >= 1 ? 'lob' : 'moonball';
logSwing({ result: 'THEM', reason: `${kind} q=${r2(quality)} · ${Math.round(Math.hypot(ball.vx, ball.vz))}m/s${mark}`
+ (ball.approach === ch.side ? ' · coming in' : '') });
+ (ball.approach === ch.side ? ' · coming in' : ''), px: r2(P.x), pz: r2(P.z) }); // where HE stood when it left
}
G.wasServe = false; // a serve volleyed back pre-bounce must not fault-check the return
ball.pow = quality; // carried pace: zombies splatter only above zSplatQ
@@ -1618,7 +1618,8 @@ function logWhiff() {
else reason = 'unclassified — in reach, in window, in height band'; // should not happen
}
logSwing({ result: 'MISS', reason: reason + ` · ${P.flick ? 'flick' : 'tap'} ${P.tap || 0}ms`, dMin: r2(best.d), y: r2(best.y), tSwing: r2(best.t),
ballZ: r2(best.bz), bounces: best.bounces, px: r2(P.x), pz: r2(P.z) });
ballZ: r2(best.bz), bounces: best.bounces, px: r2(P.x), pz: r2(P.z),
...(P.tgt ? { runTo: r2(P.tgt.x) + ',' + r2(P.tgt.z) } : {}) }); // where the auto-run was taking him
}
function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
@@ -1647,9 +1648,12 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
// the bounce will still land inside this window. If it will not, this is the last chance and the
// low ball is dug out as a half-volley (the below-the-tape cap prices it): a swing a touch early
// used to WHIFF here, which read as the game eating a good swing (Oliver's log, 2026-10-14).
// The deadline is the window AFTER the bounce (the groundstroke one, shorter than the volley window
// this swing is running on now) — judged against the long one, it waited and then expired on the
// bounce frame (his v367 log, 8.98 s).
if (letBounce()) {
const gg = ball.g || TUNE.gravity, tb = (ball.vy + Math.sqrt(ball.vy * ball.vy + 2 * gg * ball.y)) / gg;
if (t + tb < TUNE.swingWindup + win) return;
if (t + tb < TUNE.swingWindup + TUNE.swingWindow * winLevel() - 0.03) return;
}
// quality: how centered in the window the contact lands
const center = TUNE.swingWindup + win / 2;
@@ -2027,7 +2031,10 @@ export function keyPressed(code, lead = 0, flick = false) {
// gutem Timing nicht mehr bekommen"). input.js hands the touch's age in as `lead`, and the swing
// clock starts that far along: the timing is judged from touch-down, the ball still has to be
// there for the contact.
P.swingT = rallyFeel() ? lead : 0; P.pose = 'swing'; P.poseT = 0; G.swung = true;
// A FLICK is NOT back-dated by default (TUNE.flickLead 0): his v367 log showed every 62-64 ms flick
// landing 76-92 ms EARLY once v366 timed flicks from touch-down — a flick is timed by its swipe,
// not by the finger landing. The touch's age is still logged for both gestures.
P.swingT = rallyFeel() ? (flick ? Math.min(lead, TUNE.flickLead) : lead) : 0; P.pose = 'swing'; P.poseT = 0; G.swung = true;
P.tap = Math.round(lead * 1000); P.flick = flick; // ...and the log says which gesture, and how long it took to fire
}
}
+4 -5
View File
@@ -207,11 +207,10 @@ cv.addEventListener('touchmove', e => {
? { ax: Math.abs(dx) > dist * 0.38 ? Math.sign(dx) : 0,
az: Math.abs(dy) > dist * 0.38 ? Math.sign(dy) : 0,
fx: dx / dist, fy: dy / dist, mag: dist } : null;
// ...timed, like the tap, from the moment the finger LANDED (gameplay.keyPressed's `lead`): the
// timed gold ring means "touch now", and a flick only swings once it has travelled flickMin.
// Capped at flickLead, so a slow drag still has a window to swing into (the left-flick lesson).
// The touch's age rides along for the swing log; keyPressed decides how much of it the swing
// clock is back-dated by (TUNE.flickLead — 0: a flick is timed by its swipe, his v367 log).
if (G.swipe && !stick.swung && G.phase === 'rally') {
stick.swung = true; keyPressed('Space', Math.min(TUNE.flickLead, (performance.now() - stick.t0) / 1000), true);
stick.swung = true; keyPressed('Space', (performance.now() - stick.t0) / 1000, true);
}
}
}, { passive: false });
@@ -240,7 +239,7 @@ const endTouch = e => {
az: Math.abs(dy) > dist * 0.38 ? Math.sign(dy) : 0,
fx: dx / dist, fy: dy / dist, mag: dist, // raw vector: splatter free-aim
t: performance.now() };
if (!stick.swung) keyPressed('Space', Math.min(TUNE.flickLead, dt / 1000), true); // the crossing already swung — never twice per gesture
if (!stick.swung) keyPressed('Space', dt / 1000, true); // the crossing already swung — never twice per gesture
}
stick = null; touchAxis.x = 0; touchAxis.z = 0; G.swipe = null;
}