The fall anticipates the pass: a decided winner resolves mid-flight
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
1e601ca33d
commit
098aefeb63
1 file changed
+40
-2
+40
-2
@@ -941,6 +941,9 @@ function matchFallWillPlay() {
|
|||||||
return racketStage({ hp: after, hpMax: rz.hpMax }) !== racketStage(rz); // a limb
|
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
|
// 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
|
// 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
|
// 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;
|
if (!rz || ball.vz > 0) return false;
|
||||||
const g = ball.g || TUNE.gravity;
|
const g = ball.g || TUNE.gravity;
|
||||||
const t2 = Math.max(0.05, 2 * ball.vy / g); // time until the second touchdown
|
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)
|
const spd = rzChaseSpeed(rz);
|
||||||
* ((rz.foe && rz.foe.speed) || 1);
|
|
||||||
for (let i = 0; i <= 8; i++) {
|
for (let i = 0; i <= 8; i++) {
|
||||||
const t = t2 * i / 8;
|
const t = t2 * i / 8;
|
||||||
const bx = ball.x + ball.vx * t, bz = ball.z + ball.vz * t;
|
const bx = ball.x + ball.vx * t, bz = ball.z + ball.vz * t;
|
||||||
@@ -960,6 +962,25 @@ function matchOutOfReach() {
|
|||||||
return true;
|
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() {
|
function onBounce() {
|
||||||
ball.bounces++;
|
ball.bounces++;
|
||||||
pock(0.35, 500);
|
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
|
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) {
|
if (ball.y <= 0 && ball.vy < 0) {
|
||||||
// EXACT TOUCHDOWN, not the frame after it. The integrated position lands up to one full
|
// 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
|
// step PAST the true contact — at serve pace that is ~0.75 m of travel in 1/60 s, so a
|
||||||
|
|||||||
Reference in new issue
Block a user