From 098aefeb63cab573590d0610c8d4c15e38549ad5 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Thu, 13 Aug 2026 22:06:41 +0200 Subject: [PATCH] The fall anticipates the pass: a decided winner resolves mid-flight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Once the player's winner is past the net its landing is closed-form knowable (predictLanding is exact), so a ball that will bounce in beyond the body's provable reach resolves while still in the air — the collapse starts as the ball flies past, roughly half a second earlier than the bounce-1 call, which stays as the fallback for anything too close to call. Same conservative margins, same willFall gate, same decided-latch discipline. Co-Authored-By: Claude Fable 5 --- src/gameplay.js | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/gameplay.js b/src/gameplay.js index 2bfd23d..2eb3078 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -941,6 +941,9 @@ function matchFallWillPlay() { return racketStage({ hp: after, hpMax: rz.hpMax }) !== racketStage(rz); // a limb } +const rzChaseSpeed = rz => TUNE.racketSpeed + * (TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer) * ((rz.foe && rz.foe.speed) || 1); + // Can the match opponent still reach this ball before its second bounce? Closed-form flight from // the POST-BOUNCE state (velocities already damped when the line call runs) against the body's own // chase speed — the same dials racketAI moves with — plus a full swing reach and a 0.6 m benefit @@ -950,8 +953,7 @@ function matchOutOfReach() { if (!rz || ball.vz > 0) return false; const g = ball.g || TUNE.gravity; const t2 = Math.max(0.05, 2 * ball.vy / g); // time until the second touchdown - const spd = TUNE.racketSpeed * (TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer) - * ((rz.foe && rz.foe.speed) || 1); + const spd = rzChaseSpeed(rz); for (let i = 0; i <= 8; i++) { const t = t2 * i / 8; const bx = ball.x + ball.vx * t, bz = ball.z + ball.vz * t; @@ -960,6 +962,25 @@ function matchOutOfReach() { return true; } +// The MID-FLIGHT version (Oliver: "the losing point could still be more anticipated"): the same +// question asked while the ball is still in the air, over the piecewise track flight -> bounce -> +// roll to the second touchdown. Same conservative margins; pred is predictLanding()'s exact spot. +function matchOutOfReachFlight(rz, pred) { + const g = ball.g || TUNE.gravity; + const spd = rzChaseSpeed(rz); + const vyl = ball.vy - g * pred.t; // vertical speed at touchdown + const t2 = Math.max(0.05, 2 * (-vyl * TUNE.bounceY) / g); + const vx2 = ball.vx * TUNE.bounceXZ, vz2 = ball.vz * TUNE.bounceXZ; + const T = pred.t + t2; + for (let i = 0; i <= 10; i++) { + const t = T * i / 10; + const bx = t <= pred.t ? ball.x + ball.vx * t : pred.x + vx2 * (t - pred.t); + const bz = t <= pred.t ? ball.z + ball.vz * t : pred.z + vz2 * (t - pred.t); + if (Math.hypot(bx - rz.x, bz - rz.z) <= spd * t + TUNE.aiReach + 0.6) return false; + } + return true; +} + function onBounce() { ball.bounces++; pock(0.35, 500); @@ -1752,6 +1773,23 @@ export function update(dt) { G.netRipple = { x: ball.x, t: 0.4 }; // the tape carries the hit: a damped wobble where it clipped } } + // THE FALL ANTICIPATES THE PASS (Oliver: "the losing point could still be more anticipated + // timing-wise"). Once the player's winner is past the net its landing is closed-form knowable + // (predictLanding is exact), so a ball that will bounce in and is provably beyond the body's + // reach resolves MID-FLIGHT — the collapse starts while the ball is still in the air. The + // bounce-1 hook in onBounce stays as the fallback for anything too close to call up here. + if (G.match && !G.match.ending && !G.match.decided && G.phase === 'rally' + && ball.active && ball.lastHit === 'P' && ball.vz < 0 && ball.z < COURT.netZ - 0.3 + && matchFallWillPlay()) { + const arz = zombies.find(z => z.variant === 'r'); + const pred = arz && predictLanding(); + if (pred && inCourt(pred.x, pred.z) && matchOutOfReachFlight(arz, pred)) { + G.matchEarly = true; + resolveInterlude('P'); + G.matchEarly = false; + if (G.match) G.match.decided = true; + } + } if (ball.y <= 0 && ball.vy < 0) { // EXACT TOUCHDOWN, not the frame after it. The integrated position lands up to one full // step PAST the true contact — at serve pace that is ~0.75 m of travel in 1/60 s, so a