diff --git a/PROJECT.md b/PROJECT.md index 4b1d2c0..5879839 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -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. diff --git a/index.html b/index.html index cf525f2..433e52a 100644 --- a/index.html +++ b/index.html @@ -1140,6 +1140,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat - + diff --git a/src/config.js b/src/config.js index c3446ed..7026e90 100644 --- a/src/config.js +++ b/src/config.js @@ -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 diff --git a/src/gameplay.js b/src/gameplay.js index 1f11f14..729c04e 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -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 } } diff --git a/src/input.js b/src/input.js index dea90e4..75891b1 100644 --- a/src/input.js +++ b/src/input.js @@ -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; }