From 4052ab14045c6c8a9375240369462d1057b16477 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Thu, 20 Aug 2026 14:39:10 +0200 Subject: [PATCH] The relative lane is vetoed; an aimed ball never lands on the wrong side of you MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'No that's way too difficult now - I am dying constantly of out. revert it.' aimAnchor is gone — the lane is absolute again, every out invariant back exactly as tuned. What survives is the answer to his follow-up: the ball sometimes flew a LITTLE RIGHT out of a left flick, because a mistimed left flick's target sits only slightly left of CENTRE — which is right of a player standing in the left service box. The absolute lane's blind spot, stated the other way round. The aimed target is now clamped to the player's own x on the flicked side, bounded at flickWidth so a body parked in the alley clamps to the corner rather than conjuring an out. From the left box a left flick goes at least down your own line, from centre it can no longer cross centre, a perfect corner flick and the crosscourt sweep are untouched, and the outward scatter tail keeps the whole tuned out-risk. Verified in his scene: ten rushed weak left flicks land -2.0..-2.7 where the same case used to target -0.7..-1.3 (a visibly rightward flight); the clean full flick still lands -3.92. Co-Authored-By: Claude Fable 5 --- PROJECT.md | 14 +++++++++++++ index.html | 2 +- src/config.js | 8 -------- src/gameplay.js | 30 ++++++++++++++------------- test.js | 54 +++++++++++++++++++++---------------------------- 5 files changed, 54 insertions(+), 54 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index d964f03..fb382ba 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -615,6 +615,20 @@ opponent's lanes and the pOut invariants are stated from centre and unchanged th his exact scene (both bodies in their left boxes): full left flick lands −5 (the alley), half swipe −3.07 (hunting the line, in), full right +1.93 (a full sweep measured from him). +**...VETOED the same day and replaced by the wrong-side guard** (2026-08-20: *"no that's way too +difficult now - I am dying constantly of out. revert it."*). The relative lane is gone +(`aimAnchor` deleted; git has it if ever wanted). What survives is the answer to his follow-up — +*"the ball sometimes actually flies a little to the right, although I flicked left. how can this +be?"* — which is the absolute lane's blind spot stated the other way round: a mistimed left +flick's target sits only slightly left of CENTRE, and that is RIGHT of a player standing in the +left box, so the ball visibly flew rightward out of a left flick. **The aimed target is now +clamped to the player's own x on the flicked side** (bounded at ±`flickWidth`, so a body parked +in the alley clamps to the corner and can never conjure an out): from the left box a left flick +goes at least down your own line, from centre it can no longer cross centre, a perfect corner +flick and the crosscourt sweep are untouched, and the outward scatter tail — the whole tuned +out-risk — is unchanged. Verified in his scene: ten rushed weak left flicks landed −2.0..−2.7 +(previously targets −0.7..−1.3 — a rightward flight); the clean full flick still lands −3.92. + **The umpire and the boy.** `aside()` refused during any interlude and the match runs on a permanent one, so he was silently muted for the entire mode — the one mode that is literally a tennis match he is sitting over (`G.interlude && !G.match` now, the same shape as `sendBoy`). Six dry lines: two on diff --git a/index.html b/index.html index cd01c6f..7defda3 100644 --- a/index.html +++ b/index.html @@ -659,6 +659,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/config.js b/src/config.js index 1f38c0d..82555a4 100644 --- a/src/config.js +++ b/src/config.js @@ -64,14 +64,6 @@ export const TUNE = { // send the ball longer — solveShot still lands it on the same spot — it // gets there flatter and sooner, which is what a smash is. aimError: 2.4, // m of scatter at quality 0 - aimAnchor: 1, // how much the flick's lane follows the PLAYER instead of the court. - // 0 = the old absolute lane (a full flick asks for flickWidth off the - // court's centre — saturates near a sideline: from x -2 a perfect left - // flick moved the ball under 2 m and could never find the line). 1 = the - // anchor slides to your own x as the swipe commits, so a full flick is a - // full sweep from where you stand — including past the line: out-by-aim - // is a choice now, and the ring already turns red for it. Blendable so - // the panel can A/B the feel live. Player's rally lanes only. aimTrust: 0.3, // an aimed rally shot TRUSTS its flick: the scatter may still push the // ball further OUT (the chosen out-risk keeps its exact shape) but only // this fraction of an INWARD roll survives, so a mistimed left flick diff --git a/src/gameplay.js b/src/gameplay.js index a0acbd8..923df1a 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -189,20 +189,22 @@ function doHit(ch, quality, aim) { // symmetric scatter is its width mechanic (matchWide clamps it, not this). let e = err(); if (ch.side === 'P' && sway && e * Math.sign(sway) < 0) e *= TUNE.aimTrust; - // THE FLICK AIMS RELATIVE TO YOU, not to the court (2026-08-20, third round of the same - // report: "when the opponent and the player are both standing in the left service box it is - // virtually impossible to flick the ball into the left out. no matter how hard/good I flick"). - // The lane was ABSOLUTE — a full flick asked for 3.95 m from the court's centre — so standing - // near a sideline saturated the gesture: from x -2 a perfect left flick moved the ball under - // 2 m and could never pass the line, while the same swipe from centre swept the court. The - // anchor now slides from centre to the player's own x as the swipe commits (|sway|), so a - // flat tap is exactly what it always was and a full flick is a full sweep FROM WHERE YOU - // STAND — including past the line: out-by-aim is his explicit ask ("not about the possibility - // of the ball going out but rather the aiming itself"). TUNE.aimAnchor blends it (0 = the old - // absolute lane, 1 = fully relative) so the panel can A/B the feel live. Player only; the - // opponent's lanes and every pOut invariant are stated from centre and unchanged there. - const anchor = ch.side === 'P' ? ch.x * Math.abs(sway) * TUNE.aimAnchor : 0; - tx = anchor + sway * (toFar ? TUNE.flickWidth : 2.7) * reach + e; + tx = sway * (toFar ? TUNE.flickWidth : 2.7) * reach + e; + // ...AND AN AIMED BALL NEVER LANDS ON THE WRONG SIDE OF YOU. The lane is ABSOLUTE (a fully + // relative lane was tried 2026-08-20 and feel-vetoed the same day — "way too difficult now, + // I am dying constantly of out"), and an absolute lane has a blind spot: a mistimed left + // flick's target sits only slightly left of CENTRE, which is RIGHT of a player standing in + // the left service box — so the ball visibly flew right out of a left flick ("the ball + // sometimes actually flies a little to the right, although I flicked left"). The target is + // clamped to the player's own x on the flicked side now (bounded at flickWidth, so a body + // parked in the alley cannot conjure an out): from the left box a left flick goes at least + // down your own line, from centre it can no longer cross centre, and a perfect corner flick + // is untouched. No new out risk — the clamp only ever pulls the target TOWARD the line the + // player is standing on, never past flickWidth, and the outward scatter tail is unchanged. + if (ch.side === 'P' && sway) { + const bound = Math.max(-TUNE.flickWidth, Math.min(TUNE.flickWidth, ch.x)); + tx = sway < 0 ? Math.min(tx, bound) : Math.max(tx, bound); + } const tzBase = aim.az < 0 ? SHOT_DEPTH.deep : aim.az > 0 ? SHOT_DEPTH.short : SHOT_DEPTH.mid; tz = (toFar ? tzBase : COURT.L - tzBase) + err() * 0.8; // A FORWARD FLICK IS A SMASH. Up-screen is the far court, so flicking INTO the court asked for diff --git a/test.js b/test.js index 1f491d8..fd86baa 100644 --- a/test.js +++ b/test.js @@ -876,38 +876,30 @@ test('an aimed shot keeps its DIRECTION: scatter may push it wider, never back p 'aimTrust*aimError must stay under a committed swipe\'s worst-case aim'); }); -test('the flick aims RELATIVE TO THE PLAYER: a full sweep from where you stand, not from centre', () => { - // Third round of the service-line report: the lane was absolute, so standing near a sideline - // saturated the gesture — from the left box a perfect left flick moved the ball under 2 m and - // could never find the line ("no matter how hard/good I flick"). The anchor follows the player - // as the swipe commits (TUNE.aimAnchor); a flat tap keeps its old centre default. - startMatch(); - G.modal = null; G.modalQ.length = 0; - const arm = (px, swipe) => { // one rally ball, contact at the window's centre - G.interlude.introT = 0; G.phase = 'rally'; - P.x = px; P.z = 22.8; P.swingT = -1; G.placeMark = null; - ball.active = true; ball.lastHit = 'O'; ball.bounces = 1; - ball.x = px; ball.y = 0.7; ball.z = P.z - 4.15; - ball.vx = 0; ball.vy = 2.0; ball.vz = 14; ball.g = TUNE.gravity; - G.swipe = swipe; - keyPressed('Space'); - for (let i = 0; i < 30 && ball.lastHit !== 'P'; i++) update(DT); - assert.equal(ball.lastHit, 'P', 'contact happened'); - G.swipe = null; - return G.placeMark; +test('an aimed ball never lands on the WRONG SIDE of you', () => { + // The absolute lane's blind spot: a mistimed left flick's target sits only slightly left of + // CENTRE — which is RIGHT of a player standing in the left box, so the ball visibly flew right + // out of a left flick. (The fully relative lane that fixed this harder was feel-vetoed the same + // day: "way too difficult now, I am dying constantly of out".) Model mirrors doHit's lane path. + const land = (px, s, q, u) => { + const reach = TUNE.flickReach + (1 - TUNE.flickReach) * q; + let e = u * TUNE.aimError * (1 - q); + if (s && e * Math.sign(s) < 0) e *= TUNE.aimTrust; + let tx = s * TUNE.flickWidth * reach + e; + const bound = Math.max(-TUNE.flickWidth, Math.min(TUNE.flickWidth, px)); + return s < 0 ? Math.min(tx, bound) : Math.max(tx, bound); }; - const full = fx => ({ ax: Math.sign(fx), az: 0, fx, fy: 0, mag: TUNE.flickFull }); - const line = COURT.W / 2; - // from the LEFT service box, a committed left flick reaches PAST the line — out is aimable now - const left = arm(-2, full(-1)); - assert.ok(left && left.x < -line, 'full left flick from the left box finds the alley: x=' + left.x.toFixed(2)); - // ...and a right flick from the same spot lands a full sweep right OF THE PLAYER, near centre-right - const right = arm(-2, full(1)); - assert.ok(right && right.x > 1.2 && right.x < 2.8, 'full right flick sweeps from the player: x=' + right.x.toFixed(2)); - // an unaimed tap is untouched by where you stand: no mark, and the ball heads for the centre - assert.equal(arm(-2, null), null, 'a flat tap from the box stamps no mark, exactly as before'); - assert.ok(Math.abs(ball.vx * ((3 - ball.z) / ball.vz) + ball.x) < TUNE.aimError + 1, - 'and its line stays the old centre default, not the player lane'); + for (let q = 0; q <= 1; q += 0.25) for (const u of [-1, 0, 1]) for (const s of [-0.2, -0.6, -1]) { + assert.ok(land(-2, s, q, u) <= -2, 'a left flick from the left box never crosses the player: ' + + land(-2, s, q, u).toFixed(2) + ' at s=' + s + ' q=' + q + ' u=' + u); + assert.ok(land(0, s, q, u) <= 0, '...and from centre it never crosses centre'); + } + assert.equal(land(-2, -1, 1, 0), -TUNE.flickWidth, 'a perfect corner flick is untouched by the guard'); + assert.equal(land(2, -1, 1, 0), -TUNE.flickWidth, 'crosscourt from the right box keeps its full sweep'); + // the guard can never conjure an out: the bound is capped inside the line + assert.ok(TUNE.flickWidth < COURT.W / 2, 'flickWidth stays inside the singles line'); + assert.ok(Math.abs(Math.max(-TUNE.flickWidth, Math.min(TUNE.flickWidth, -6.5))) <= TUNE.flickWidth, + 'a body parked in the alley clamps to the corner, not past the line'); }); test('the flick aims from the LIVE swipe: contact reads G.swipe, no release needed', () => {