Make the run to the net and back a double tap

Oliver: "das Vorlaufen und Zurücklaufen sollte durch einen Doppeltipp
ausgelöst werden". It replaces v374's flick-length rule: the long
up-flick is only the deep ball again, the long down-flick only the lob.

While his own ball flies there is nothing to hit, so a tap there is not
a swing. Two inside 0.35 s take him in to the net from the back, or back
from the net. "TO THE NET" or "BACK" pops under him. An incoming ball
keeps every tap a swing, so the two meanings never share a moment. After
his own serve it is serve and volley. The short-ball approach stays
automatic.

Measured: the base ladder is unchanged, and following the ball in still
pays against pushers and costs against big hitters. Three tests replace
v374's two; v376.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5.5 committed 2026-10-14 20:30:38 +02:00
1 parent bb72a01a3e
commit 48d1069e40
7 files changed
+107 -37

No files matched your search

+46 -21
View File
@@ -1521,11 +1521,11 @@ test('LOBBED at the net, the player backpedals too, and only blocks it back', ()
} finally { TUNE.autoAssist = assist; }
});
// ---- THE PLAYER'S WAY TO THE NET AND BACK, AND THE SMASH (2026-10-14, Oliver: "Steuerung ans Netz gehen"
// ---- THE PLAYER'S WAY TO THE NET AND BACK (the double tap), AND THE SMASH (2026-10-14, Oliver: "Steuerung ans Netz gehen"
// ... "wenn man einen Lob vom Gegner als Volley annimmt, soll man derb schmettern") ---------------------
// One player shot with a given swipe from (x 0, z pz), then `secs` of flight with the opponent parked out
// of play. Returns the shot's flags and where the player's own run took him.
function ownShot(swipe, pz, secs = 1.2) {
function ownShot(swipe, pz, secs = 1.2, taps = []) {
freshSplatter(); startMatch(); G.interlude.introT = 0;
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
try {
@@ -1538,30 +1538,55 @@ function ownShot(swipe, pz, secs = 1.2) {
G.swipe = swipe;
update(DT); G.swipe = null;
assert.equal(ball.lastHit, 'P', 'the swing connects');
const out = { approach: ball.approach, retreat: ball.retreat };
for (let i = 0; i < secs * 60 && ball.active; i++) { rz.swingCool = 999; update(DT); }
out.fromNet = Math.abs(P.z - COURT.netZ);
return out;
let swung = false;
for (let i = 0; i < secs * 60 && ball.active; i++) {
rz.swingCool = 999;
if (taps.includes(i)) { keyPressed('Space', 0.1); swung ||= P.swingT >= 0; }
update(DT);
}
return { approach: ball.approach, retreat: ball.retreat, fromNet: Math.abs(P.z - COURT.netZ), swung };
} finally { TUNE.autoAssist = assist; }
}
test('THE ATTACK: a long up-flick follows the ball to the net, a short one stays back', () => {
const up = mag => ({ ax: 0, az: -1, fx: 0, fy: -1, mag });
const attack = ownShot(up(TUNE.attackMin + 30), 24), deep = ownShot(up(TUNE.attackMin - 30), 24);
assert.equal(attack.approach, 'P', 'the long up-flick is the attack');
assert.ok(attack.fromNet < TUNE.netFrom, `...and he is at the net by the time it gets there: ${attack.fromNet.toFixed(1)} m`);
assert.equal(deep.approach, null, 'the short one is the deep ball');
assert.ok(deep.fromNet > TUNE.netFrom + 3, `...and he stays back: ${deep.fromNet.toFixed(1)} m`);
test('THE DOUBLE TAP is his legs: from the back it takes him to the net, a single tap does nothing', () => {
// Oliver, 2026-10-14: "das Vorlaufen und Zurücklaufen sollte durch einen Doppeltipp ausgelöst werden".
// While his own ball flies there is nothing to hit, so taps there are the move, never a swing.
const deep = { ax: 0, az: -1, fx: 0, fy: -1, mag: TUNE.flickFull + 20 };
const plain = ownShot(deep, 24), one = ownShot(deep, 24, 1.2, [6]), two = ownShot(deep, 24, 1.2, [6, 6 + 10]);
assert.equal(plain.approach, null, 'a long up-flick is only a shot now: no run');
assert.ok(plain.fromNet > TUNE.netFrom + 3, `...he stays back: ${plain.fromNet.toFixed(1)} m`);
assert.ok(!one.swung && one.approach === null && one.fromNet > TUNE.netFrom + 3, 'one tap while his ball flies: nothing at all');
assert.equal(two.approach, 'P', 'the double tap is the way in');
assert.ok(!two.swung, '...and neither tap swung');
assert.ok(two.fromNet < TUNE.netFrom, `...and he is at the net by the time the ball gets there: ${two.fromNet.toFixed(1)} m`);
const slow = ownShot(deep, 24, 1.2, [6, 6 + Math.ceil(TUNE.dblTap * 60) + 3]);
assert.equal(slow.approach, null, 'two taps further apart than dblTap are not a double tap');
});
test('THE WAY BACK: a long down-flick from the net is the lob, and he goes back behind it; a drop keeps the net', () => {
const down = mag => ({ ax: 0, az: 1, fx: 0, fy: 1, mag });
const lob = ownShot(down(TUNE.flickFull + 20), COURT.netZ + TUNE.netStand, 2);
const drop = ownShot(down(TUNE.dropMax - 30), COURT.netZ + TUNE.netStand, 1);
assert.equal(lob.retreat, 'P', 'the lob is the way back');
assert.ok(lob.fromNet > TUNE.netFrom, `...and he is back behind the net zone: ${lob.fromNet.toFixed(1)} m`);
assert.equal(drop.retreat, null, 'a drop volley is not a retreat');
assert.ok(drop.fromNet < TUNE.netFrom, `...he holds the net: ${drop.fromNet.toFixed(1)} m`);
test('THE DOUBLE TAP at the net is the way back; a long down-flick is only the lob', () => {
const down = { ax: 0, az: 1, fx: 0, fy: 1, mag: TUNE.flickFull + 20 };
const lob = ownShot(down, COURT.netZ + TUNE.netStand, 1.5);
assert.equal(lob.retreat, null, 'the lob alone does not take him back');
assert.ok(lob.fromNet < TUNE.netFrom, `...he holds the net: ${lob.fromNet.toFixed(1)} m`);
const back = ownShot(down, COURT.netZ + TUNE.netStand, 1.5, [6, 14]);
assert.equal(back.retreat, 'P', 'the double tap at the net is the way back');
assert.ok(back.fromNet > TUNE.netFrom, `...and he is back behind the net zone: ${back.fromNet.toFixed(1)} m`);
});
test('an INCOMING ball keeps every tap a swing: the double tap never eats a shot', () => {
freshSplatter(); startMatch(); G.interlude.introT = 0;
oppOf().swingCool = 999;
G.phase = 'rally'; G.wasServe = false;
const v = solveShot(0, 1, 2, 0.5, 21, 20);
Object.assign(ball, { active: true, scored: false, lastHit: 'O', bounces: 0, rx: null, spin: 1, soft: false,
x: 0, y: 1, z: 2, vx: v.vx, vy: v.vy, vz: v.vz, g: TUNE.gravity });
P.swingT = -1;
keyPressed('Space', 0.1);
assert.ok(P.swingT >= 0, 'the first tap swings');
P.swingT = -1;
keyPressed('Space', 0.1);
assert.ok(P.swingT >= 0, 'and so does the second');
assert.equal(ball.approach, null, 'no run was called');
});
test('THE SMASH: a lob taken out of the air is struck hard to the open court; a deep lob still beats him', () => {