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

+29 -1
View File
@@ -5709,6 +5709,33 @@ flipping by metres, every other frame, between two candidates:
200 ms. The old code makes 26. The early-tap test's floor moved 0.45 → 0.42: he now stops 0.12 m from his 200 ms. The old code makes 26. The early-tap test's floor moved 0.45 → 0.42: he now stops 0.12 m from his
mark instead of 0.35, so contact comes a hair later. The old curve still reads 0.33. mark instead of 0.35, so contact comes a hair later. The old curve still reads 0.33.
### The run in and out is a DOUBLE TAP (2026-10-14, v376)
Oliver on v375: *"das Vorlaufen und Zurücklaufen sollte durch einen Doppeltipp ausgelöst werden"*. It
replaces v374's flick-length rule. The shot and the run are separate decisions now: the long up-flick is
only the deep hard ball, the long down-flick only the lob.
- **While his OWN ball flies** (rally modes, `ball.lastHit === 'P'`) there is nothing to hit, so a tap there
is not a swing at all. Two taps inside `dblTap` (0.35 s) are `moveCall`: from the back to the net
(`ball.approach = 'P'`), from the net (or while already running in) back (`ball.retreat`). A word pops
under him ("TO THE NET" / "BACK"). A single tap there does nothing (it used to be a pointless swish).
- **An incoming ball keeps every tap a swing**, so the two meanings never share a moment. The rule from
the 2026-08-22 lob holds: the meaning depends on whose ball is in the air, which he always knows, never
on where the opponent stands.
- **After his own serve it is serve and volley.** `P.rushing` no longer excludes the serve. His reply is a
net man's plan from wherever his run has got to.
- **The short-ball approach stays automatic** (step 1, decision (c)). A double tap during that run takes him
back.
- Input: a tap fires at lift if under 250 ms (input.js), and the canvas has `touch-action: none`, so the
browser's double-tap zoom never sees it.
- Measured (σ60, level 26, 8 seeds; the bot follows 30% of its deep flicks in with a double tap): the base
ladder is unchanged. Attacking still pays against the pushers (Steward 52 → 61) and costs against the big
hitters and the Champion (Brute 29 → 20, Champion 33 → 25). It is a little weaker than v374's, because he
sets off after the shot, not with it.
- Tests (172): a double tap from the back takes him in, one tap or two too far apart do nothing and never
swing, the long up-flick alone no longer runs; a double tap at the net takes him back, the lob alone does
not; an incoming ball keeps both taps swings.
- **Not built:** a lesson for it. The ghost hand teaches the tap and the side flick only; the double tap
is undiscoverable without being told. Add one if the phone test says so.
# OPEN — leftovers, in rough priority order # OPEN — leftovers, in rough priority order
Nothing here is broken; these are known gaps and unverified risks. Nothing here is broken; these are known gaps and unverified risks.
@@ -5723,7 +5750,8 @@ Nothing here is broken; these are known gaps and unverified risks.
v374 adds the player's own net game: long up-flick = attack and follow in, long down-flick = lob and v374 adds the player's own net game: long up-flick = attack and follow in, long down-flick = lob and
go back, and the SMASH. See "The player's way to the net and back" above. go back, and the SMASH. See "The player's way to the net and back" above.
v375: the auto-run's shake is gone (a latched chase plan); he is a little stronger for it, so the bottom v375: the auto-run's shake is gone (a latched chase plan); he is a little stronger for it, so the bottom
of the Climb and classic easy may now be too easy. See "The auto-run stops shaking". Fallback tag `rally-baseline-v360`. of the Climb and classic easy may now be too easy. See "The auto-run stops shaking".
v376: the run to the net and back is a DOUBLE TAP while his own ball flies (replaces v374's flick length). Fallback tag `rally-baseline-v360`.
1. **`RES` 4 is unverified on a real phone under load.** It nearly doubled the fill rate (810 -> 1080 1. **`RES` 4 is unverified on a real phone under load.** It nearly doubled the fill rate (810 -> 1080
device px across). Oliver has not reported stutter, but he also has not played a late horde since. device px across). Oliver has not reported stutter, but he also has not played a late horde since.
+1 -1
View File
@@ -1140,6 +1140,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
</div> </div>
</div> </div>
<span id="recLamp" hidden>REC</span> <span id="recLamp" hidden>REC</span>
<script type="module" src="src/main.js?v=375"></script> <script type="module" src="src/main.js?v=376"></script>
</body> </body>
</html> </html>
+1 -3
View File
@@ -107,9 +107,7 @@ export const TUNE = {
softErr: 0.8, // ...and how far toward it at quality 0 softErr: 0.8, // ...and how far toward it at quality 0
lobTurn: 0.6, // leg speed of an opponent lobbed at the net: he turns and backpedals lobTurn: 0.6, // leg speed of an opponent lobbed at the net: he turns and backpedals
netReact: 0.22, // s an opponent at the net stands reading the pass before he moves netReact: 0.22, // s an opponent at the net stands reading the pass before he moves
attackMin: 90, // px: an UP-flick this long is the ATTACK — the deep ball and you follow it to the dblTap: 0.35, // s: two taps this close while his OWN ball flies are the move — to the net, or back
// net (shorter: the deep ball, you stay). Mirror of dropMax: a long DOWN-flick is
// the lob and takes you back to the baseline. One meaning each, wherever you stand
smashMinY: 1.9, // m: a ball the player takes OUT OF THE AIR at least this high is a SMASH... smashMinY: 1.9, // m: a ball the player takes OUT OF THE AIR at least this high is a SMASH...
smashMaxY: 2.5, // ...up to this high (above CONTACT.maxY: the overhead reaches higher than a volley — smashMaxY: 2.5, // ...up to this high (above CONTACT.maxY: the overhead reaches higher than a volley —
// but under the 2.8 m the opponent's lob clears a net man by, or he smashes it standing) // but under the 2.8 m the opponent's lob clears a net man by, or he smashes it standing)
+22 -8
View File
@@ -393,13 +393,7 @@ function doHit(ch, quality, aim) {
// Never against a man already at the net — that ball is a PASS, and two volleyers is the old // Never against a man already at the net — that ball is a PASS, and two volleyers is the old
// reflex duel at a quarter of a second per shot. ball.from is the striker's distance from the net. // reflex duel at a quarter of a second per shot. ball.from is the striker's distance from the net.
ball.approach = rallyFeel() && ball.rx && ball.rx.short && !(ball.from < TUNE.netFrom) ? ch.side : null; ball.approach = rallyFeel() && ball.rx && ball.rx.short && !(ball.from < TUNE.netFrom) ? ch.side : null;
// THE PLAYER'S OWN WAY IN AND OUT (2026-10-14, Oliver: "Steuerung ans Netz gehen"). A LONG up-flick ball.retreat = null; // his own way back is a DOUBLE TAP while this ball flies (moveCall), never the shot
// is the attack: the deep ball, and he follows it in — even onto a man at the net, it is his call.
// A long DOWN-flick is the lob, and he goes back behind it. Gesture length only, never context: the
// 2026-08-22 lob taught that a gesture whose meaning depends on where the opponent stands is noise.
const longFlick = aim && (aim.mag === undefined || aim.mag >= TUNE.attackMin);
if (ch.side === 'P' && rallyFeel() && aim && aim.az < 0 && !aim.smash && longFlick) ball.approach = 'P';
ball.retreat = ch.side === 'P' && rallyFeel() && soft !== null && soft >= 1 ? 'P' : null;
ball.smash = !!(aim && aim.smash); ball.smash = !!(aim && aim.smash);
ball.from = Math.abs(ch.z - COURT.netZ); ball.from = Math.abs(ch.z - COURT.netZ);
ball.lastHit = ch.side; ball.bounces = 0; ball.rx = null; // a new shot: the receiver plans it afresh ball.lastHit = ch.side; ball.bounces = 0; ball.rx = null; // a new shot: the receiver plans it afresh
@@ -2139,6 +2133,15 @@ export function keyPressed(code, lead = 0, flick = false) {
hitServe(P, q, sway || hard ? { sx: sway, hard } : null); hitServe(P, q, sway || hard ? { sx: sway, hard } : null);
return; return;
} }
// THE DOUBLE TAP IS HIS LEGS (2026-10-14, Oliver: "das Vorlaufen und Zurücklaufen sollte durch einen
// Doppeltipp ausgelöst werden"). While his OWN ball is flying there is nothing to hit, so a tap there is
// not a swing: two inside dblTap are the move — in to the net from the back, back from the net. An
// incoming ball keeps every tap a swing, so the two meanings never share a moment. (v374 put this on the
// flick's length; the shot and the run are separate decisions now.)
if (rallyFeel() && G.phase === 'rally' && ball.active && ball.lastHit === 'P' && !flick) {
if (G.moveTapT > 0) { G.moveTapT = 0; moveCall(); } else G.moveTapT = TUNE.dblTap;
return;
}
if (G.phase === 'rally' && P.swingT < 0) { if (G.phase === 'rally' && P.swingT < 0) {
recordEvent('swing', { window: TUNE.swingWindow * winLevel(), assist: assistK(), lead }); recordEvent('swing', { window: TUNE.swingWindow * winLevel(), assist: assistK(), lead });
// A TAP COUNTS FROM THE MOMENT THE FINGER LANDS (rally modes). A tap is only known to BE a tap // A TAP COUNTS FROM THE MOMENT THE FINGER LANDS (rally modes). A tap is only known to BE a tap
@@ -2156,6 +2159,16 @@ export function keyPressed(code, lead = 0, flick = false) {
} }
} }
// In to the net, or back from it: the double tap's one meaning is "the other end of my half". At the net
// (or already running in) it is the way back; anywhere else it is the way in. rallyHome reads the flags
// while his ball flies, the run goes at full pace, and his reply is planned from wherever he has got to.
function moveCall() {
const goingIn = ball.approach === 'P' || (Math.abs(P.z - COURT.netZ) < TUNE.netFrom && ball.retreat !== 'P');
ball.approach = goingIn ? null : 'P';
ball.retreat = goingIn ? 'P' : null;
popText(P.x, 0, P.z + TUNE.praiseBehind, goingIn ? 'BACK' : 'TO THE NET', UI.warm, true);
}
function expirePose(ch, dt) { function expirePose(ch, dt) {
if (ch.pose === 'swing' || ch.pose === 'serve') { ch.poseT += dt; if (ch.poseT > POSE_TIME) ch.pose = 'idle'; } if (ch.pose === 'swing' || ch.pose === 'serve') { ch.poseT += dt; if (ch.poseT > POSE_TIME) ch.pose = 'idle'; }
} }
@@ -2225,6 +2238,7 @@ function mopRound(b, dt) {
// ---------------------------------------------------------------- update ---- // ---------------------------------------------------------------- update ----
export function update(dt) { export function update(dt) {
recordFrame(dt); recordFrame(dt);
if (G.moveTapT > 0) G.moveTapT -= dt; // the double tap's window (keyPressed)
if (G.hitstop > 0) { G.hitstop -= dt; return; } if (G.hitstop > 0) { G.hitstop -= dt; return; }
// THE OTHER ENDING: a classic match played out. The world holds still — the man is standing, the // THE OTHER ENDING: a classic match played out. The world holds still — the man is standing, the
// crowd is still clapping it down — and the only thing running is the clock the summary and its // crowd is still clapping it down — and the only thing running is the clock the summary and its
@@ -2353,7 +2367,7 @@ export function update(dt) {
// ...and lobbed at the net he backpedals, exactly as slowly as the CPU does (TUNE.lobTurn) // ...and lobbed at the net he backpedals, exactly as slowly as the CPU does (TUNE.lobTurn)
// HIS OWN WAY IN AND OUT runs at full pace (the attack's follow-in, the lob's retreat), and while // HIS OWN WAY IN AND OUT runs at full pace (the attack's follow-in, the lob's retreat), and while
// his attack flies he is RUSHING: the reply is planned as a net man's from wherever he has got to // his attack flies he is RUSHING: the reply is planned as a net man's from wherever he has got to
if (ball.lastHit === 'P') P.rushing = ball.approach === 'P' && !G.wasServe; if (ball.lastHit === 'P') P.rushing = ball.approach === 'P'; // a double tap behind his serve is serve and volley
const ownRun = !chasing && rallyFeel() && ball.lastHit === 'P' && (ball.approach === 'P' || ball.retreat === 'P'); const ownRun = !chasing && rallyFeel() && ball.lastHit === 'P' && (ball.approach === 'P' || ball.retreat === 'P');
let rate = TUNE.playerSpeed * aK * dt * (chasing || ownRun ? 1 : 0.55) * (chasing && ball.rx && ball.rx.lob ? TUNE.lobTurn : 1); let rate = TUNE.playerSpeed * aK * dt * (chasing || ownRun ? 1 : 0.55) * (chasing && ball.rx && ball.rx.lob ? TUNE.lobTurn : 1);
// ON HIS HEELS: a put-away of his own sitter is read late (step 4 (b)). Without this the auto-run // ON HIS HEELS: a put-away of his own sitter is read late (step 4 (b)). Without this the auto-run
+1
View File
@@ -184,6 +184,7 @@ export const G = {
rallyNear: null, // closest the incoming ball got this rally (for never-swung passes) rallyNear: null, // closest the incoming ball got this rally (for never-swung passes)
swung: false, // player swung at least once this rally — whiff log supersedes PASSED swung: false, // player swung at least once this rally — whiff log supersedes PASSED
lastMiss: null, // {text, at} — most recent miss reason, drawn on the HUD briefly lastMiss: null, // {text, at} — most recent miss reason, drawn on the HUD briefly
moveTapT: 0, // the double tap's open window (gameplay.keyPressed, moveCall)
ring: null, // the drawn gold ring's steady cue (gameplay update smooths ringCue) ring: null, // the drawn gold ring's steady cue (gameplay update smooths ringCue)
logShow: false, // tap the scoreboard to toggle the on-screen miss-log dump logShow: false, // tap the scoreboard to toggle the on-screen miss-log dump
}; };
+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; } } 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") --------------------- // ... "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 // 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. // 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; freshSplatter(); startMatch(); G.interlude.introT = 0;
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15; const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
try { try {
@@ -1538,30 +1538,55 @@ function ownShot(swipe, pz, secs = 1.2) {
G.swipe = swipe; G.swipe = swipe;
update(DT); G.swipe = null; update(DT); G.swipe = null;
assert.equal(ball.lastHit, 'P', 'the swing connects'); assert.equal(ball.lastHit, 'P', 'the swing connects');
const out = { approach: ball.approach, retreat: ball.retreat }; let swung = false;
for (let i = 0; i < secs * 60 && ball.active; i++) { rz.swingCool = 999; update(DT); } for (let i = 0; i < secs * 60 && ball.active; i++) {
out.fromNet = Math.abs(P.z - COURT.netZ); rz.swingCool = 999;
return out; 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; } } finally { TUNE.autoAssist = assist; }
} }
test('THE ATTACK: a long up-flick follows the ball to the net, a short one stays back', () => { test('THE DOUBLE TAP is his legs: from the back it takes him to the net, a single tap does nothing', () => {
const up = mag => ({ ax: 0, az: -1, fx: 0, fy: -1, mag }); // Oliver, 2026-10-14: "das Vorlaufen und Zurücklaufen sollte durch einen Doppeltipp ausgelöst werden".
const attack = ownShot(up(TUNE.attackMin + 30), 24), deep = ownShot(up(TUNE.attackMin - 30), 24); // While his own ball flies there is nothing to hit, so taps there are the move, never a swing.
assert.equal(attack.approach, 'P', 'the long up-flick is the attack'); const deep = { ax: 0, az: -1, fx: 0, fy: -1, mag: TUNE.flickFull + 20 };
assert.ok(attack.fromNet < TUNE.netFrom, `...and he is at the net by the time it gets there: ${attack.fromNet.toFixed(1)} m`); const plain = ownShot(deep, 24), one = ownShot(deep, 24, 1.2, [6]), two = ownShot(deep, 24, 1.2, [6, 6 + 10]);
assert.equal(deep.approach, null, 'the short one is the deep ball'); assert.equal(plain.approach, null, 'a long up-flick is only a shot now: no run');
assert.ok(deep.fromNet > TUNE.netFrom + 3, `...and he stays back: ${deep.fromNet.toFixed(1)} m`); 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', () => { test('THE DOUBLE TAP at the net is the way back; a long down-flick is only the lob', () => {
const down = mag => ({ ax: 0, az: 1, fx: 0, fy: 1, mag }); const down = { ax: 0, az: 1, fx: 0, fy: 1, mag: TUNE.flickFull + 20 };
const lob = ownShot(down(TUNE.flickFull + 20), COURT.netZ + TUNE.netStand, 2); const lob = ownShot(down, COURT.netZ + TUNE.netStand, 1.5);
const drop = ownShot(down(TUNE.dropMax - 30), COURT.netZ + TUNE.netStand, 1); assert.equal(lob.retreat, null, 'the lob alone does not take him back');
assert.equal(lob.retreat, 'P', 'the lob is the way back'); assert.ok(lob.fromNet < TUNE.netFrom, `...he holds the net: ${lob.fromNet.toFixed(1)} m`);
assert.ok(lob.fromNet > TUNE.netFrom, `...and he is back behind the net zone: ${lob.fromNet.toFixed(1)} m`); const back = ownShot(down, COURT.netZ + TUNE.netStand, 1.5, [6, 14]);
assert.equal(drop.retreat, null, 'a drop volley is not a retreat'); assert.equal(back.retreat, 'P', 'the double tap at the net is the way back');
assert.ok(drop.fromNet < TUNE.netFrom, `...he holds the net: ${drop.fromNet.toFixed(1)} m`); 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', () => { test('THE SMASH: a lob taken out of the air is struck hard to the open court; a deep lob still beats him', () => {
+7 -3
View File
@@ -93,7 +93,7 @@ const med = a => { const s = [...a].sort((x, y) => x - y); return s.length ? s[M
// 120 ms tap), and plays the game a little — a lob or a pass against a net man, the odd drop when // 120 ms tap), and plays the game a little — a lob or a pass against a net man, the odd drop when
// the opponent hangs back. BOT=dist is the original fixed-distance tapper with random flicks. // the opponent hangs back. BOT=dist is the original fixed-distance tapper with random flicks.
const HUMAN = (process.env.BOT || 'human') === 'human'; const HUMAN = (process.env.BOT || 'human') === 'human';
const ATTACK = Number(process.env.ATTACK || 0); // share of baseline shots played as the attack (long up-flick) const ATTACK = Number(process.env.ATTACK || 0); // share of baseline shots followed in: a deep flick, then a DOUBLE TAP
const JIT = Number(process.env.JIT ?? 60) / 1000, LAG = 7; const JIT = Number(process.env.JIT ?? 60) / 1000, LAG = 7;
const gauss = () => Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random()); const gauss = () => Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random());
const netx = { attacks: 0, smash: 0, smashWon: 0, lobbed: 0, open: null, p0: 0 }; const netx = { attacks: 0, smash: 0, smashWon: 0, lobbed: 0, open: null, p0: 0 };
@@ -108,7 +108,7 @@ function humanAim() {
const away = rz && rz.x > 0 ? -1 : 1; // the side he is not on const away = rz && rz.x > 0 ? -1 : 1; // the side he is not on
if (oz < TUNE.netFrom) return r < 0.4 ? sw(0, 1, 150) : sw(away, -0.3, 120); // net man: lob him or pass him if (oz < TUNE.netFrom) return r < 0.4 ? sw(0, 1, 150) : sw(away, -0.3, 120); // net man: lob him or pass him
if (oz > COURT.netZ + 0.5 && Math.abs(P.z - COURT.netZ) < COURT.netZ - 1.5 && r < 0.15) return sw(0, 1, 50); // drop if (oz > COURT.netZ + 0.5 && Math.abs(P.z - COURT.netZ) < COURT.netZ - 1.5 && r < 0.15) return sw(0, 1, 50); // drop
if (ATTACK && Math.abs(P.z - COURT.netZ) >= TUNE.netFrom && r < ATTACK) return sw(away * 0.3, -1, 130); // the attack: follow it in if (ATTACK && Math.abs(P.z - COURT.netZ) >= TUNE.netFrom && Math.random() < ATTACK) { bot.dbl = true; return sw(away * 0.3, -1, 130); } // the attack
if (r < 0.45) return sw(away, -0.2, 110); // to the open side if (r < 0.45) return sw(away, -0.2, 110); // to the open side
if (r < 0.65) return sw(0, -1, 80); // deep through the middle if (r < 0.65) return sw(0, -1, 80); // deep through the middle
return null; // a plain tap return null; // a plain tap
@@ -162,6 +162,8 @@ for (const rung of RUNGS) {
} }
} }
if (i === bot.fire) { G.swipe = bot.swipe; keyPressed('Space', bot.swipe ? 0 : LAG / 60, !!bot.swipe); } if (i === bot.fire) { G.swipe = bot.swipe; keyPressed('Space', bot.swipe ? 0 : LAG / 60, !!bot.swipe); }
if (bot.dbl && ball.lastHit === 'P' && ball.active) { bot.dbl = false; bot.taps = [i + 4, i + 12]; } // follow it in: the double tap
if (bot.taps && bot.taps.includes(i)) keyPressed('Space', 0.1);
} }
else if (G.phase === 'rally' && ball.active && ball.lastHit === 'O') { else if (G.phase === 'rally' && ball.active && ball.lastHit === 'O') {
const d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8); const d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8);
@@ -178,7 +180,9 @@ for (const rung of RUNGS) {
const px0 = P.x, pz0 = P.z, tg0 = P.tgt; const px0 = P.x, pz0 = P.z, tg0 = P.tgt;
if (process.env.PROBE && lh === 'O' && bot.land && P.swingT >= 0) bot.land.sw = P.swingT, bot.land.d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8), bot.land.y = ball.y, bot.land.bb = ball.bounces; if (process.env.PROBE && lh === 'O' && bot.land && P.swingT >= 0) bot.land.sw = P.swingT, bot.land.d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8), bot.land.y = ball.y, bot.land.bb = ball.bounces;
update(DT); update(DT);
if (lh !== 'P' && ball.lastHit === 'P' && ball.active) { if (ball.approach === 'P' && !G.wasServe) netx.attacks++; if (ball.smash) { netx.smash++; netx.open = 'smash'; } } if (lh !== 'P' && ball.lastHit === 'P' && ball.active && ball.smash) { netx.smash++; netx.open = 'smash'; }
if (ball.lastHit === 'P' && ball.approach === 'P' && !netx.wasIn) netx.attacks++; // a run in (the double tap, or a short ball)
netx.wasIn = ball.lastHit === 'P' && ball.approach === 'P';
if (lh === 'P' && ball.lastHit === 'O' && ball.active && ball.soft && Math.abs(P.z - COURT.netZ) < TUNE.netFrom) netx.lobbed++; if (lh === 'P' && ball.lastHit === 'O' && ball.active && ball.soft && Math.abs(P.z - COURT.netZ) < TUNE.netFrom) netx.lobbed++;
if (netx.open && (G.phase !== 'rally' || !ball.active)) { const d0 = score.pts[0] - netx.p0; if (d0 > 0) netx.smashWon++; netx.open = null; } if (netx.open && (G.phase !== 'rally' || !ball.active)) { const d0 = score.pts[0] - netx.p0; if (d0 > 0) netx.smashWon++; netx.open = null; }
if (netx.open === null) netx.p0 = score.pts[0]; if (netx.open === null) netx.p0 = score.pts[0];