Let the player take the net and come back, and smash a lob he gets under

Oliver: "Steuerung ans Netz gehen", option (1), and "einen Lob vom Gegner
als Volley annehmen soll derb schmettern".

- A long up-flick (90 px or more) is the attack: the deep ball, and he
  follows it in. A long down-flick is the lob, and he goes back behind
  it. Short flicks keep the deep ball / the drop and leave him where he
  is. Length only, never context.
- The net costs him what it costs the CPU: he split-steps (netReact)
  before reading the pass, and a CPU facing a net man passes at the
  line. Without both, the attacker won 78-95% of his net points.
- The smash: a ball taken out of the air at 1.9-2.5 m is struck harder
  and to the open court, read late by the opponent, and pops "SMASH!".
  It wins ~80% of its points.
- Lobbed at the net, he decides once whether he can get under it. If he
  can, he smashes. If not, he runs it down and only blocks it back, even
  if he meets it in the air. Re-deciding every frame smashed every lob.

Measured at level 26: the base ladder is unchanged. Attacking pays
against pushers and touch players and costs against big hitters,
slicers and rushers. Three new tests; v374.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5.5 committed 2026-10-14 20:05:59 +02:00
1 parent 4406aeafdb
commit 599d5b0506
8 files changed
+201 -17

No files matched your search

+73
View File
@@ -1486,6 +1486,79 @@ 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"
// ... "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) {
freshSplatter(); startMatch(); G.interlude.introT = 0;
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
try {
const rz = oppOf();
Object.assign(rz, { x: 3, z: -0.6, swingCool: 999, swingT: -1 });
G.phase = 'rally'; G.wasServe = false;
Object.assign(P, { x: 0, z: pz, swingT: TUNE.swingWindup + TUNE.swingWindow * winLevelNow() / 2 - DT });
Object.assign(ball, { active: true, scored: false, lastHit: 'O', bounces: 1, rx: { net: false, short: false },
spin: 1, soft: false, x: 0.4, y: 1.0, z: pz - 0.4, vx: 0, vy: -0.5, vz: 3, g: TUNE.gravity });
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;
} 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 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 SMASH: a lob taken out of the air is struck hard to the open court; a deep lob still beats him', () => {
// the opponent's lob from its baseline, at the player standing at the net; a gold-ring tap, as a player would
const lobAt = (tz, speed) => {
freshSplatter(); startMatch(); G.interlude.introT = 0;
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
try {
const zb = oppOf(); Object.assign(zb, { x: 2, z: 0.5, swingCool: 999 });
G.phase = 'rally'; G.wasServe = false;
Object.assign(P, { x: 0, z: COURT.netZ + TUNE.netStand, swingT: -1 });
const v = solveShot(0, 1, 2, 0, tz, speed);
Object.assign(ball, { active: true, scored: false, lastHit: 'O', bounces: 0, rx: null, spin: 1, soft: true, pow: 0.8,
heels: 0, x: 0, y: 1, z: 2, vx: v.vx, vy: v.vy, vz: v.vz, g: TUNE.gravity });
let tapped = false;
for (let i = 0; i < 4 * 60 && ball.lastHit === 'O' && ball.active; i++) {
zb.swingCool = 999;
if (!tapped && ringCue()?.gold) { tapped = true; keyPressed('Space'); }
update(DT);
}
return { hit: ball.lastHit === 'P', smash: ball.smash, q: ball.pow, pace: Math.hypot(ball.vx, ball.vz),
land: ball.lastHit === 'P' ? predictLanding() : null };
} finally { TUNE.autoAssist = assist; }
};
const short = lobAt(COURT.L - 6, 9), deep = lobAt(COURT.L - 1.5, 9);
assert.ok(short.hit && short.smash, 'a short lob is smashed');
assert.ok(short.q > 0.6 && short.pace > 30, `...hard: q ${short.q.toFixed(2)}, ${short.pace.toFixed(1)} m/s`);
assert.ok(Math.abs(short.land.x) > 2.9, `...into the open court: lands at x ${short.land.x.toFixed(1)}`);
assert.ok(!deep.smash, 'a deep lob is not smashed — he turns and runs it down');
if (deep.hit) assert.ok(deep.q <= TUNE.lowVolleyQ + 1e-9, 'and only blocks it back');
});
test('THE SWING LOG writes in the match too: a whiff, a hit with its tap, and why the point went', () => {
// Oliver, 2026-10-14: "bitte bau nochmal die Funktion ein die protokolliert warum ich einen Ball nicht
// bekommen habe". It was gated on G.mode === 'tennis' and the match runs as 'splatter' — the Climb