From 7ff8671a22f9e28cbf78ef594e816a4bcd78d11b Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Tue, 18 Aug 2026 07:58:47 +0200 Subject: [PATCH] The aim cloud goes quiet, and a mistimed flick falls short of the line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hidden behind TUNE.aimCloud rather than deleted: the reading it gives — how much width the swipe is buying, before the ball leaves — is the only teacher the gesture has, and the panel can flip it back live for the next look at it. And "a little harder to hit the out" is a number: flickReach 0.62 -> 0.52, which halves the worst-case rate from about 15% to about 7%, with the test tightened so it cannot drift back. The shape is untouched — a perfect flick still never sails, a badly timed full swipe still can — but a mistimed one now lands nearer the middle than the line, which is the only half of that trade a player who cannot see his own timing can do anything about. Co-Authored-By: Claude Opus 5 --- PROJECT.md | 12 +++++++++++- index.html | 2 +- src/config.js | 18 ++++++++++++++---- src/render.js | 2 +- test.js | 3 +++ 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index 3b5a965..48bac38 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -502,6 +502,13 @@ swayOf` reads the swipe's length as a continuous 0..1 (`TUNE.flickMin`..`flickFu the SERVE already did, and `doHit` spends that instead of `ax`. Keys have no length, so they keep the full lane, and the AI's aim (no `mag`) is untouched. +**...and the out was tightened once more** (2026-08-18, *"make it a little harder to hit the out"*): +`flickReach` 0.62 -> 0.52, halving the worst-case rate from ~15% to ~7% and pinned by the test. The +shape is unchanged — a perfect flick still never sails, a badly timed full swipe still can — but a +mistimed one now lands NEARER THE MIDDLE rather than nearer the line, which is the only half of that +trade a player who cannot see his own timing can do anything about. **`flickReach` is the out dial**; +`aimError` is shared with the opponent's returns and with every aimed shot in the gore modes. + **This is also what hands the OUT back to the player.** Oliver: *"the number one reason i die is that i myself keep firing balls into the out."* Going out was never his choice — every aimed shot took the FULL width and then `err()` on top, so the dice decided. Width follows the swipe now, so @@ -540,7 +547,10 @@ timing can keep in. Three things were learned making it read, all of them the ha obvious ramp showed nothing across the whole safe half of its range. The cool end carries its contrast in LIGHTNESS, which is what makes it read on grass, clay and slab alike. The threshold is not a flip: the colour is already gold before you reach it, so "too hard" is -something you watch coming. Only inside the window where a release would still BE a flick (`FLICK_MS`/`TUNE.flickMin`, the same +something you watch coming. **It is OFF as of its first playtest** (*"hide the indicator for now"*) +— `TUNE.aimCloud` gates the whole block and the tuning panel can flip it back live. Kept rather than +deleted: it is the only thing that shows the gesture's strength before the ball leaves, and the +lesson it teaches has no other teacher. Only inside the window where a release would still BE a flick (`FLICK_MS`/`TUNE.flickMin`, the same gate `endTouch` uses) — past that the gesture is a run and a target hanging off it would be a lie. Rally modes only (`rallyFeel`); gore's flick free-aims at a body, not at a lane. The placement ring turns `UI.bad` when the shot really is out, so the two rings together ARE the lesson: what you asked diff --git a/index.html b/index.html index 0d22d60..a51f555 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 ad116ff..c08011a 100644 --- a/src/config.js +++ b/src/config.js @@ -69,12 +69,17 @@ export const TUNE = { flickWidth: 3.95, // m off centre a PERFECTLY timed flick reaches. The singles line is 4.115, // so this is the corner with 17 cm to spare. Raising it past the line does // not make the flick punchier, it makes a perfect one go out. - flickReach: 0.62, // how much of an aimed shot's width a MISTIMED flick gets (1 = all of it). + flickReach: 0.52, // how much of an aimed shot's width a MISTIMED flick gets (1 = all of it). // 0.42 made going out IMPOSSIBLE, which fixed the 8-in-10 bug and then // overshot it: "flicking: too easy. i must be able to play out." At 0.62 - // a mistimed flick CAN sail (~15% of the time at quality 0, on the worst - // half of the scatter) and a perfectly timed one never can — the risk is - // back and it is timing, not a coin toss, that decides it. + // a mistimed flick sailed ~15% of the time at quality 0 and a perfectly + // timed one never could — the risk was back, and timing rather than a + // coin toss decided it. 0.52 keeps that shape and halves the worst-case + // rate to ~7% ("make it a little harder to hit the out"): a badly timed + // flick now lands NEARER THE MIDDLE rather than nearer the line, which + // is the only half of the trade a player who cannot see his own timing + // can do anything about. THIS is the out dial — aimError is shared with + // the opponent's returns and with every aimed shot in the gore modes. // Reach x quality must stay under the sideline once aimError is added on // top, or the flick goes out — which is exactly the bug this fixed: // 3.3 * 0.45 + 2.4 = 3.89 against a 4.11 m line. Raise it and the flick @@ -86,6 +91,11 @@ export const TUNE = { // of agency, the player can't really tell if there was skill or luck"). The serve already read its // flick as a continuous strength; the rally does now too, on the same shape of numbers. flickMin: 28, // px before a drag counts as a flick at all — input.js's own threshold + aimCloud: 0, // 1 draws the live aim cloud under the moving thumb (render's G.swipe + // block); 0 hides it. OFF after its first playtest — "hide the indicator + // for now" — kept as a switch rather than deleted because the reading it + // gives (how much width the swipe is buying, before the ball leaves) is + // the only teacher the gesture has, and the panel can flip it back live. flickFull: 130, // ...and px of travel that buys the FULL width. About a third of a phone // screen: a short swipe is a safe half-court ball, and only a committed // one goes hunting the line, which is what makes going OUT a chosen risk. diff --git a/src/render.js b/src/render.js index 092b8eb..b94543e 100644 --- a/src/render.js +++ b/src/render.js @@ -2294,7 +2294,7 @@ export function render() { // walks out toward the corner as you swipe further, and turns red once you are asking for a ball // the line cannot hold. Timing still decides how much of it you actually get — that is what the // receipt says, and the difference between the two rings is exactly the lesson. - if (G.swipe && G.phase === 'rally') { + if (G.swipe && G.phase === 'rally' && TUNE.aimCloud) { const sway = swayOf(G.swipe); const x = sway * TUNE.flickWidth; const z = G.swipe.fy < -0.38 ? SHOT_DEPTH.deep : G.swipe.fy > 0.38 ? SHOT_DEPTH.short : SHOT_DEPTH.mid; diff --git a/test.js b/test.js index 4387747..53b45c0 100644 --- a/test.js +++ b/test.js @@ -807,6 +807,9 @@ test('an aimed return can be played OUT, but only by mistiming it', () => { }; assert.ok(pOut(0) > 0.02, 'a badly timed flick CAN go out: ' + (pOut(0) * 100).toFixed(1) + '%'); assert.ok(pOut(0) < 0.25, '...but not the coin toss it was (every flick under q 0.66 went out)'); + // ...and "make it a little harder to hit the out" (2026-08-18) is a NUMBER, not a vibe: the + // worst-case rate was halved off 15%, and this is what stops flickReach drifting back up. + assert.ok(pOut(0) < 0.10, 'a mistimed flick lands nearer the middle than the line: ' + (pOut(0) * 100).toFixed(1) + '%'); assert.equal(pOut(1), 0, 'a perfectly timed flick never goes out'); for (let q = 0.05; q <= 1.0001; q += 0.05) assert.ok(pOut(q) <= pOut(q - 0.05) + 1e-9, `risk falls as timing improves (q ${q.toFixed(2)})`);