The fall beats the ball: a passing winner fells the body while it still rolls

Oliver: 'the zombie falls after the ball is lost although we know this
earlier — it could play out as ball lost because of the falling.' The match
point now resolves at bounce ONE when the winner is provably beyond the
body's reach (closed-form flight vs its own chase dials, full swing reach
plus 0.6 m of doubt — borderline balls wait for the ordinary bounce-2 call).
startFall keeps the ball rolling for an early-decided point, and the
d.decided latch swallows the rally's later resolutions so it can never bank
twice. Verified in-pane: collapse starts ~0.3 s after the bounce, ball rolls
past the prone body, score banks exactly once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-13 21:35:52 +02:00
1 parent c6218a6284
commit 53df079db6
4 files changed
+64 -2

No files matched your search

+24
View File
@@ -761,6 +761,30 @@ test('a wounding point knocks the body down and it gets up MINUS the limb', () =
assert.equal(G.serveNum, 1, 'matchReady ran: the next point is armed');
});
test('the fall beats the ball: a passing winner fells the body while the ball still rolls', () => {
// Oliver: "the zombie falls after the ball is lost although we know this earlier — it could
// play out as ball lost BECAUSE of the falling." A winner that bounced in beyond the body's
// provable reach resolves at bounce ONE: the collapse starts, the ball keeps rolling, and the
// decided-latch makes sure the rally can never bank twice.
startMatch();
G.mode = 'splatter'; // the app's intoSplatter() precedes startMatch()
G.interlude.introT = 0;
const zb = oppOf();
zb.hp = 3; // so THIS point crosses a wound stage (3 -> 2 is the arm)
zb.x = -4; zb.z = 2; // parked far from where the winner lands
ball.active = true; ball.lastHit = 'P'; ball.bounces = 0; ball.wid = 'ball'; ball.collected = false;
ball.x = 3.4; ball.z = 7; ball.y = 0.5; ball.vx = 0.5; ball.vy = -2; ball.vz = -6; ball.g = TUNE.gravity;
G.phase = 'rally'; G.wasServe = false;
const pts = score.pts[0];
for (let i = 0; i < 40 && !G.matchFall; i++) update(DT);
assert.ok(G.matchFall, 'the collapse starts at the decided moment, not at the dead ball');
assert.ok(ball.active, '...and the ball is still rolling past the falling body');
assert.equal(score.pts[0], pts + 1, 'the point banked at the decision');
for (let i = 0; i < 5 * 60; i++) update(DT); // the ball dies its ordinary death, the fall plays out
assert.equal(score.pts[0], pts + 1, '...and never banked a second time');
assert.ok(!G.match.decided, 'the latch cleared for the next rally');
});
test('service alternates by game, and the opponent actually serves', () => {
// Until now the corpse only ever returned: score.server was pinned to 'P' because the match calls
// advancePoint directly and the flip lives in tennis's pointTo, which the match bypasses. Return