The lob is out: a gesture the opponent's position redefines is unpredictable

Oliver, on the build: "all good but the lob. please remove it - this is
totally unpredictable."

Reverted in full — oppAtNet, farBody, TUNE.lobArm, TUNE.lobApex,
SHOT_DEPTH.lob, the branch in doHit and its two tests. The forward flick is
the smash it always was. Everything else from that commit stays: the pace
feedback (streak, crack, camera kick) and the death match's blood alignment.

The reason is worth more than the code, and PROJECT.md carries it as a rule:
a gesture whose MEANING depends on where the opponent is standing is
unpredictable however well the shot is tuned. It is the same objection
input.js already states about the slingshot — the direction you moved is the
thing you asked for, no invisible modes — and the written comparison let
option (c) past it on the argument that context cannot mis-fire. It can: you
do not track where the far body is standing while a ball is coming at you, so
one thumb movement produced two shots for reasons you were not watching. The
LOB word at contact explained the surprise; it did not prevent it.

If it comes back it needs a gesture of its own. The ballistics are solved and
are in the history at 4f4534e.

111 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-22 13:13:56 +02:00
1 parent 4f4534e272
commit a42dc6fc81
5 files changed
+41 -146

No files matched your search

+1 -61
View File
@@ -89,7 +89,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
// ---- game modules (real code) ----------------------------------------------
const state = await import('./src/state.js');
const { G, P, O, score, ball, setupPoint } = state;
const { update, forceZombies, startGore, splatPoint, restartRun, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt, oppAtNet } = await import('./src/gameplay.js');
const { update, forceZombies, startGore, splatPoint, restartRun, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN, startNetVs, endNetVs } = await import('./src/beats.js');
const { mirror, netMsg, netTest, netTick } = await import('./src/net.js');
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
@@ -736,49 +736,6 @@ test('a receiver stands BEHIND his baseline, on both sides of the net', () => {
});
test('THE LOB: the forward flick goes OVER him once he has come in', () => {
// Oliver, 2026-08-22: "when you're close to the net in a duel there should be a gesture allowing
// to make a ball flying high from below." Option (c): no new gesture — the flick that already
// means "deep" picks the trajectory that actually puts the ball deep, which with a man at the net
// is over his head. The contract: unchanged while he is back, unhittable while he is in.
const shot = (oz, swipe) => {
const rnd = Math.random; Math.random = () => 0.5; // no scatter: the NOMINAL arc
startMatch(); G.interlude.introT = 0; G.phase = 'rally';
const zb = oppOf(); zb.x = 0; zb.z = oz;
P.x = 0; P.z = 23; P.swingT = -1;
ball.active = true; ball.lastHit = 'O'; ball.bounces = 1;
ball.x = 0; ball.z = 22.6; ball.y = 1.0; ball.vx = 0; ball.vy = 0; ball.vz = 1; ball.g = TUNE.gravity;
G.swipe = swipe; keyPressed('Space');
for (let i = 0; i < 20 && ball.lastHit !== 'P'; i++) update(DT);
G.swipe = null; Math.random = rnd;
assert.equal(ball.lastHit, 'P', 'setup: he got a racket on it');
let y = ball.y, z = ball.z, vy = ball.vy, apex = y, over = null;
const g = ball.g, dt = 1 / 240;
for (let i = 0; i < 4000 && y > 0; i++) {
const z0 = z; vy -= g * dt; y += vy * dt; z += ball.vz * dt;
apex = Math.max(apex, y);
if (over === null && z0 > zb.z && z <= zb.z) over = y; // height as it passes him
}
return { s: Math.hypot(ball.vx, ball.vz), apex, over, land: z };
};
const UP = { ax: 0, az: -1, fx: 0, fy: -1, mag: 90 };
const back = shot(1.5, UP);
assert.ok(back.apex < 2.5, 'he is on his baseline: the forward flick is the DRIVE it always was, apex ' + back.apex.toFixed(1));
assert.ok(back.s > 19, '...at drive pace: ' + back.s.toFixed(1) + ' m/s');
// ...and once he steps in, the SAME gesture lifts it over him. Two positions: the far service
// line (as far back as the racket zombie's chase ever brings it) and the edge of the arm band.
for (const oz of [9.5, 5.5, COURT.netZ - TUNE.lobArm + 0.05]) {
const l = shot(oz, UP);
assert.ok(Math.abs(l.apex - TUNE.lobApex) < 0.2, 'it peaks at lobApex from anywhere: ' + l.apex.toFixed(1));
assert.ok(l.over > CONTACT.maxY, 'it passes him at ' + l.over.toFixed(2) + 'm, over his ' + CONTACT.maxY + 'm reach (z ' + oz + ')');
assert.ok(l.land > 0.3 && l.land < COURT.netZ, '...and comes down IN, at z ' + l.land.toFixed(1));
assert.ok(l.s < back.s * 0.6, '...off a much slower ball: ' + l.s.toFixed(1) + ' m/s');
}
// the sideways pass is untouched — only the FORWARD flick changes meaning
const pass = shot(5.5, { ax: 1, az: 0, fx: 1, fy: 0, mag: 90 });
assert.ok(pass.apex < 2.5 && pass.s > 19, 'a sideways flick with him at the net is still a drive');
});
test('PACE IS FELT: a fast ball kicks the camera, a slow one does not', () => {
// Oliver, 2026-08-22, after the multiplayer test: "when smashing (for example on serve) the
// opponent sees the ball really fast — the player barely sees the speed increase." The ball IS
@@ -803,23 +760,6 @@ test('PACE IS FELT: a fast ball kicks the camera, a slow one does not', () => {
assert.ok(hard > 0, '(and the fast one did: ' + hard.toFixed(1) + ')');
});
test('THE LOB arms off the BODY, not off the invisible tennis opponent', () => {
// In the match O is the body the serve machinery is driven off and it only moves between points;
// the opponent you actually play is the racket zombie. Asking O would answer "on his mark" all
// rally, so the lob would arm at the wrong times in the one mode Oliver plays most.
startMatch();
const zb = oppOf();
O.z = 10.5; // O parked at the net, the body back on its mark
zb.z = 1.5;
assert.ok(!oppAtNet(), 'the BODY is back, so nothing is armed');
zb.z = 6.0;
assert.ok(oppAtNet(), '...and the body coming in arms it');
// classic tennis has no body: there O IS the opponent
restartRun(); G.classic = true;
O.z = 1.5; assert.ok(!oppAtNet(), 'classic: O back, not armed');
O.z = 9.5; assert.ok(oppAtNet(), 'classic: O in, armed');
});
test('the match brings its own legs: auto-run works even where autoAssist is 0', () => {
// The desktop pane (pointer: fine) has autoAssist 0, so the match played as a statue with a
// racket — the flight recorder showed a swing in every lost point and an x that never moved.