From 60127436a0538d22e80fc9f128f0b57c91ca0e16 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Thu, 20 Aug 2026 14:03:57 +0200 Subject: [PATCH] An aimed shot keeps its direction: the scatter can push it wider, never back past centre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second round on the service-line flick report. The crossing-fired swing alone did not move the feel, and Oliver's sharpened observation — the flick only lands left when the opponent already stands left — pointed at a magnet that does not exist: measured with the body teleported at contact, the aim never tracks it. The real mechanism: at rushed quality the symmetric scatter (aimError x (1-q)) outweighs the quality-shrunk aim, so a mistimed left flick landed anywhere in a ~4 m blob around 'slightly left' — usually centre — and the eye anchors that on the opponent. Its position was a proxy for preparation time all along. TUNE.aimTrust: an outward scatter roll is untouched — the committed swipe's chance of sailing keeps its exact tuned shape, flickReach stays the only out dial — but an inward roll keeps only 0.3 of itself. Bad timing costs width, never direction. Player lane shots only; the opponent's symmetric scatter is its width mechanic. Verified at worst-case q~0.08: 14/14 rushed left flicks landed left, 9/9 right flicks right, where before it was a coin toss. The suite pins the direction guarantee and the aimTrust/aimError/flickReach relationship. Co-Authored-By: Claude Fable 5 --- PROJECT.md | 18 ++++++++++++++++++ index.html | 2 +- src/config.js | 8 ++++++++ src/gameplay.js | 13 ++++++++++++- test.js | 24 ++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index 872c290..6b2c281 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -579,6 +579,24 @@ serve gesture, the duel and gore are untouched; keys unchanged. Verified with re held-swipe contact aims exactly `swayOf`, a 360 ms swipe (previously dead) swings and aims, and a release-before-arrival still aims through `takeFlickAim`. +**...and an aimed shot keeps its DIRECTION** (2026-08-20, same report, second round — the crossing +fire alone "did not feel much different", and his sharpened observation was that the flick only +seemed to work when the opponent already stood where he aimed). Measured with the opponent +teleported at contact: the aim never tracked the body — no magnet exists — but at rushed quality +the SYMMETRIC scatter (±aimError·(1-q)) outweighed the quality-shrunk aim, so a mistimed left flick +landed anywhere in a ~4 m blob around "slightly left": usually centre, sometimes right, and the eye +anchors that on wherever the opponent stands. The opponent's position was a proxy for preparation +time (a body parked wide = its return flew a long slow diagonal = time = quality). The dial: +**`TUNE.aimTrust` — an OUTWARD scatter roll is untouched** (a committed mistimed swipe still sails +at the exact tuned rate; `flickReach` stays the only out dial) **but an inward roll keeps only +`aimTrust` (0.3) of itself**, so bad timing costs width, never the direction that was asked for. +Player lane shots only: the opponent's symmetric scatter is its width mechanic, and gore's +free-aim never used lanes. Verified in real rallies at worst-case q≈0.08: 14/14 rushed left flicks +landed left (−0.8..−3.5), 9/9 right flicks right — previously the same condition was a coin toss. +The suite pins the direction guarantee and the knob relationship (`aimTrust·aimError` must stay +under a committed swipe's worst-case aim). Whether placement should also BEAT an unwounded body +more often (`aiRead` on wide balls — "dial 2") is open, awaiting the phone verdict on this one. + **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 06d6b67..738a0e0 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 c08011a..82555a4 100644 --- a/src/config.js +++ b/src/config.js @@ -64,6 +64,14 @@ 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 + 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 + // lands SHORTER LEFT rather than center-or-right "wherever the dice + // said". Player's lane shots only — the opponent's symmetric scatter is + // its width mechanic, and the gore free-aim never used lanes. Must stay + // under 0.5*flickWidth*flickReach/aimError (~0.43) or a committed + // mistimed swipe can cross back past centre (pinned by the suite). // NOT `aimReach` — that name is taken further down by the slingshot's aim-ray LENGTH (22 m), and // the duplicate key silently won: every aimed shot reached 22x its width until a test caught it. flickWidth: 3.95, // m off centre a PERFECTLY timed flick reaches. The singles line is 4.115, diff --git a/src/gameplay.js b/src/gameplay.js index 47fea10..7c02991 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -178,7 +178,18 @@ function doHit(ch, quality, aim) { // it just stops being a coin toss you are punished for calling. const reach = ch.side === 'P' ? TUNE.flickReach + (1 - TUNE.flickReach) * quality : 1; sway = swayOf(aim); // -1..1, continuous on touch — see swayOf - tx = sway * (toFar ? TUNE.flickWidth : 2.7) * reach + err(); + // AN AIMED SHOT KEEPS ITS DIRECTION. The scatter used to be symmetric, and at rushed quality + // it was BIGGER than the quality-shrunk aim — a mistimed left flick landed anywhere in a 4 m + // blob around "slightly left", i.e. usually centre and sometimes right. It read as the ball + // landing wherever the opponent stood (2026-08-20, "trying to move the approaching ball with + // a flick to the left is almost impossible"). An OUTWARD roll is untouched — the committed + // swipe's chance of sailing wide is a chosen risk with a tuned shape (flickReach) and this + // must not soften it — but an inward roll now keeps only TUNE.aimTrust of itself, so bad + // timing costs WIDTH, never the direction that was asked for. Player only: the opponent's + // 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; + tx = sway * (toFar ? TUNE.flickWidth : 2.7) * reach + e; 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 292e0d6..cbab5f7 100644 --- a/test.js +++ b/test.js @@ -852,6 +852,30 @@ test('a gentle flick cannot be played out — only a committed one can', () => { assert.ok(safe > 0.4 && safe < 1, 'the ring turns hot inside the swipe range, not at either end'); }); +test('an aimed shot keeps its DIRECTION: scatter may push it wider, never back past centre', () => { + // Dial 1 (2026-08-20). At rushed quality the symmetric scatter outweighed the quality-shrunk + // aim, so a mistimed left flick landed "wherever" — usually centre, read as the ball following + // the opponent. The model mirrors doHit's lane path: outward rolls untouched (the chosen + // out-risk keeps its exact tuned shape), inward rolls keep only aimTrust of themselves. + const land = (s, q, u) => { // u in [-1,1]: the raw scatter roll + 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; + return s * TUNE.flickWidth * reach + e; + }; + const line = COURT.W / 2; + for (let q = 0; q <= 1; q += 0.1) { + assert.ok(land(0.5, q, -1) > 0, 'a committed right swipe stays right at q=' + q.toFixed(1) + + ': ' + land(0.5, q, -1).toFixed(2)); + assert.ok(land(-0.5, q, 1) < 0, '...and a committed left swipe stays left, mirrored'); + } + assert.ok(land(1, 0, 1) > line, 'the outward tail is untouched: a mistimed full swipe can still sail'); + assert.ok(land(1, 1, 1) < line, 'and a perfectly timed one still never does'); + // the knob relationship the direction guarantee rests on — retuning must keep it + assert.ok(TUNE.aimTrust * TUNE.aimError < 0.5 * TUNE.flickWidth * TUNE.flickReach, + 'aimTrust*aimError must stay under a committed swipe\'s worst-case aim'); +}); + test('the flick aims from the LIVE swipe: contact reads G.swipe, no release needed', () => { // The swing fires the moment the drag crosses flickMin now (input.js), so at contact the finger // is usually still down and no released flickAim exists yet. The aim has to come from the live