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

+1 -3
View File
@@ -107,9 +107,7 @@ export const TUNE = {
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
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
// 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
dblTap: 0.35, // s: two taps this close while his OWN ball flies are the move — to the net, or back
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 —
// 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
// 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;
// THE PLAYER'S OWN WAY IN AND OUT (2026-10-14, Oliver: "Steuerung ans Netz gehen"). A LONG up-flick
// 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.retreat = null; // his own way back is a DOUBLE TAP while this ball flies (moveCall), never the shot
ball.smash = !!(aim && aim.smash);
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
@@ -2139,6 +2133,15 @@ export function keyPressed(code, lead = 0, flick = false) {
hitServe(P, q, sway || hard ? { sx: sway, hard } : null);
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) {
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
@@ -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) {
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 ----
export function update(dt) {
recordFrame(dt);
if (G.moveTapT > 0) G.moveTapT -= dt; // the double tap's window (keyPressed)
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
// 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)
// 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
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');
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
+1
View File
@@ -184,6 +184,7 @@ export const G = {
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
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)
logShow: false, // tap the scoreboard to toggle the on-screen miss-log dump
};