Time flicks from touch-down, log the opponent's shots, save early half-volleys
From Oliver's first swing log (cup vs the Brute): 13 of 22 hits were perfect, 9 badly early or late — mostly forward-right flicks — and the weak balls became seven "passed you". Every hit was a flick, and flicks still swung only at the flickMin crossing while the timed ring says "touch now": they are now timed from touch-down too (capped at flickLead 0.1 s). The log gains its other half: a THEM line per opponent shot (kind, pace, where it lands relative to you, coming in) and "never swung" judged per incoming ball instead of once a point. And an early swing on a low ball digs it out as a capped half-volley instead of whiffing. v366. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d110e12321
commit
5bbde720ad
7 files changed
+66
-9
No files matched your search
+25
@@ -5420,6 +5420,31 @@ Oliver: *"bitte bau nochmal die Funktion ein die protokolliert warum ich einen B
|
|||||||
- Note: contact still fires on the first frame the ball is in reach, which right after a bounce is
|
- Note: contact still fires on the first frame the ball is in reach, which right after a bounce is
|
||||||
often 15-25 cm off the grass. A visual half-volley, the old contact model; untouched.
|
often 15-25 cm off the grass. A visual half-volley, the old contact model; untouched.
|
||||||
|
|
||||||
|
### Oliver's first swing log (v365, cup vs THE BRUTE) and what it changed (2026-10-14, v366)
|
||||||
|
**His question: "am I just bad or is something off".** Read off the log:
|
||||||
|
- 22 hits: 13 at q = 1, 9 under 0.5. Six of the nine bad ones were "deep right" (the forward-right
|
||||||
|
flick), which failed 6 of its 9 tries; the left flick failed 3 of 10. The bad hits split between
|
||||||
|
EARLY (the ball met at the edge of reach, d 1.55-1.73) and LATE (the ball already on top of him,
|
||||||
|
d 0.55-0.73). It is timing, with no single offset.
|
||||||
|
- Weak balls are what the Brute (power, league-5 quality) punishes. Seven LOST "passed you", and
|
||||||
|
several of them follow a weak hit of his within a second.
|
||||||
|
- Every hit said `tap 0ms`: he flicks everything. Hypothesis to check in the next log: the gore
|
||||||
|
slingshot habit. A pull-back before the forward flick crosses `flickMin` DOWNWARD, which fires the
|
||||||
|
swing early, and the live aim turns to "deep right" by contact.
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- **Flicks are timed from touch-down too** (`keyPressed(code, lead, flick)`; input.js passes the
|
||||||
|
crossing's age, capped at `flickLead` 0.1 s so a slow drag keeps a window). The timed ring means
|
||||||
|
"touch now" for both gestures. The log says `flick 46ms` / `tap 120ms`. Verified with real touch
|
||||||
|
events.
|
||||||
|
- **The log's missing half:** a THEM line per opponent shot (kind, spin, q, pace, where it lands
|
||||||
|
relative to the player, OUT, "coming in"), and `whiffed it` for a CPU flub. "Never swung" is now judged
|
||||||
|
per incoming ball: `G.swung` and `G.rallyNear` reset at the opponent's strike. It used to be judged
|
||||||
|
once a point, so none of his "passed you" lines could say whether he swung or how close the ball came.
|
||||||
|
- **An early swing on a low skidding ball no longer whiffs.** The let-it-bounce gate waits only while
|
||||||
|
the bounce will land inside the swing window. Otherwise it digs the ball out as a half-volley, capped
|
||||||
|
below the tape. His 48.14 s MISS was exactly this.
|
||||||
|
|
||||||
# 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.
|
||||||
|
|||||||
+1
-1
@@ -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=365"></script>
|
<script type="module" src="src/main.js?v=366"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -647,6 +647,7 @@ export const TUNE = {
|
|||||||
// the swing window, at any pace. 0 = the old distance ring (matchRingSweet/Band)
|
// the swing window, at any pace. 0 = the old distance ring (matchRingSweet/Band)
|
||||||
ringBand: 0.35, // share of the half-window either side of the sweet moment that draws gold (edge = q 0.65)
|
ringBand: 0.35, // share of the half-window either side of the sweet moment that draws gold (edge = q 0.65)
|
||||||
ringLead: 0.9, // s before contact the timed ring appears
|
ringLead: 0.9, // s before contact the timed ring appears
|
||||||
|
flickLead: 0.1, // s: a flick is timed from touch-down too, up to this much (a slow drag keeps a window)
|
||||||
// ---- THE READ (2026-08-28, Oliver: past the HEAD COACH "the opponents are very fast and barely
|
// ---- THE READ (2026-08-28, Oliver: past the HEAD COACH "the opponents are very fast and barely
|
||||||
// make mistakes any more — we need a twist"). The dead staff LEARN YOUR PATTERNS: land the same
|
// make mistakes any more — we need a twist"). The dead staff LEARN YOUR PATTERNS: land the same
|
||||||
// lane readHabit times in a row and the opponent has the habit — an EXPECTED ball is anticipated
|
// lane readHabit times in a row and the opponent has the habit — an EXPECTED ball is anticipated
|
||||||
|
|||||||
+27
-5
@@ -366,6 +366,20 @@ function doHit(ch, quality, aim) {
|
|||||||
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;
|
||||||
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
|
||||||
|
// THE SWING LOG's other half: what came at you. A LOST "passed you" line is unreadable without it —
|
||||||
|
// was that ball reachable at all? And "never swung" is judged per incoming ball, not once a point.
|
||||||
|
if (ch.side !== 'P' && swingLog()) {
|
||||||
|
G.swung = false; G.rallyNear = null;
|
||||||
|
const land = predictLanding(), mark = !land ? '' : (() => {
|
||||||
|
const dx = land.x - P.x, inside = r2(COURT.L - land.z);
|
||||||
|
const out = Math.abs(land.x) > COURT.W / 2 || land.z > COURT.L || land.z < COURT.netZ;
|
||||||
|
return ` · lands ${r2(Math.abs(dx))}m ${dx < 0 ? 'left' : 'right'} of you, ${inside}m inside your line${out ? ' (OUT)' : ''}`;
|
||||||
|
})();
|
||||||
|
const kind = soft === null ? (aim && aim.az < 0 ? 'deep' : 'drive') + (ball.spin > 1 ? ' topspin' : ball.spin < 1 ? ' slice' : '')
|
||||||
|
: soft === 0 ? 'drop' : soft >= 1 ? 'lob' : 'moonball';
|
||||||
|
logSwing({ result: 'THEM', reason: `${kind} q=${r2(quality)} · ${Math.round(Math.hypot(ball.vx, ball.vz))}m/s${mark}`
|
||||||
|
+ (ball.approach === ch.side ? ' · coming in' : '') });
|
||||||
|
}
|
||||||
G.wasServe = false; // a serve volleyed back pre-bounce must not fault-check the return
|
G.wasServe = false; // a serve volleyed back pre-bounce must not fault-check the return
|
||||||
ball.pow = quality; // carried pace: zombies splatter only above zSplatQ
|
ball.pow = quality; // carried pace: zombies splatter only above zSplatQ
|
||||||
ball.kills = 0; // fresh shot: multi-kill streak restarts
|
ball.kills = 0; // fresh shot: multi-kill streak restarts
|
||||||
@@ -1603,7 +1617,7 @@ function logWhiff() {
|
|||||||
else if (inWin.every(s => s.y < CONTACT.minY)) reason = `too low — ball at ${r2(best.y)}m, past you`;
|
else if (inWin.every(s => s.y < CONTACT.minY)) reason = `too low — ball at ${r2(best.y)}m, past you`;
|
||||||
else reason = 'unclassified — in reach, in window, in height band'; // should not happen
|
else reason = 'unclassified — in reach, in window, in height band'; // should not happen
|
||||||
}
|
}
|
||||||
logSwing({ result: 'MISS', reason: reason + ` · tap ${P.tap || 0}ms`, dMin: r2(best.d), y: r2(best.y), tSwing: r2(best.t),
|
logSwing({ result: 'MISS', reason: reason + ` · ${P.flick ? 'flick' : 'tap'} ${P.tap || 0}ms`, dMin: r2(best.d), y: r2(best.y), tSwing: r2(best.t),
|
||||||
ballZ: r2(best.bz), bounces: best.bounces, px: r2(P.x), pz: r2(P.z) });
|
ballZ: r2(best.bz), bounces: best.bounces, px: r2(P.x), pz: r2(P.z) });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1629,7 +1643,14 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
|||||||
const dx = ball.x - ch.x, dz = ball.z - ch.z;
|
const dx = ball.x - ch.x, dz = ball.z - ch.z;
|
||||||
if (Math.hypot(dx, dz * 0.8) > reach || ball.y > CONTACT.maxY) return;
|
if (Math.hypot(dx, dz * 0.8) > reach || ball.y > CONTACT.maxY) return;
|
||||||
if (ball.y < CONTACT.minY && ball.vy <= 0) return; // a DROPPING ball below the ankles is past you — but a rising one is a half-volley, always scoopable
|
if (ball.y < CONTACT.minY && ball.vy <= 0) return; // a DROPPING ball below the ankles is past you — but a rising one is a half-volley, always scoopable
|
||||||
if (letBounce()) return; // planned from the back: the swing waits for the bounce instead of a weak pick-up
|
// planned from the back, the swing WAITS for the bounce instead of a weak pick-up — but only while
|
||||||
|
// the bounce will still land inside this window. If it will not, this is the last chance and the
|
||||||
|
// low ball is dug out as a half-volley (the below-the-tape cap prices it): a swing a touch early
|
||||||
|
// used to WHIFF here, which read as the game eating a good swing (Oliver's log, 2026-10-14).
|
||||||
|
if (letBounce()) {
|
||||||
|
const gg = ball.g || TUNE.gravity, tb = (ball.vy + Math.sqrt(ball.vy * ball.vy + 2 * gg * ball.y)) / gg;
|
||||||
|
if (t + tb < TUNE.swingWindup + win) return;
|
||||||
|
}
|
||||||
// quality: how centered in the window the contact lands
|
// quality: how centered in the window the contact lands
|
||||||
const center = TUNE.swingWindup + win / 2;
|
const center = TUNE.swingWindup + win / 2;
|
||||||
const off = Math.abs(t - center) / (win / 2);
|
const off = Math.abs(t - center) / (win / 2);
|
||||||
@@ -1684,7 +1705,7 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
|||||||
const sway = swayOf(aim), lane = Math.abs(sway) > 0.15 ? (sway < 0 ? ' left' : ' right') : '';
|
const sway = swayOf(aim), lane = Math.abs(sway) > 0.15 ? (sway < 0 ? ' left' : ' right') : '';
|
||||||
const cap = q >= q0 ? '' : ball.rx && ball.rx.lob ? ' · capped: lobbed, on your heels'
|
const cap = q >= q0 ? '' : ball.rx && ball.rx.lob ? ' · capped: lobbed, on your heels'
|
||||||
: ball.bounces === 0 ? ' · capped: volley below the tape' : ' · capped: dug out a drop';
|
: ball.bounces === 0 ? ' · capped: volley below the tape' : ' · capped: dug out a drop';
|
||||||
logSwing({ result: 'HIT', reason: `q=${r2(q)}${kind}${lane}${volley ? ' volley' : ''} · tap ${P.tap || 0}ms${cap}`,
|
logSwing({ result: 'HIT', reason: `q=${r2(q)}${kind}${lane}${volley ? ' volley' : ''} · ${P.flick ? 'flick' : 'tap'} ${P.tap || 0}ms${cap}`,
|
||||||
d: r2(Math.hypot(dx, dz * 0.8)), y: r2(ball.y) });
|
d: r2(Math.hypot(dx, dz * 0.8)), y: r2(ball.y) });
|
||||||
}
|
}
|
||||||
doHit(ch, q, aim);
|
doHit(ch, q, aim);
|
||||||
@@ -1802,6 +1823,7 @@ function racketAI(rz, dt) {
|
|||||||
+ (G.match && G.match.surprised ? TUNE.surpFlub * learnK(rz) : 0);
|
+ (G.match && G.match.surprised ? TUNE.surpFlub * learnK(rz) : 0);
|
||||||
if (Math.random() < flub) {
|
if (Math.random() < flub) {
|
||||||
recordEvent('opponent_whiff', { chance: flub, chaseSpeed: rzChaseSpeed(rz), reach: TUNE.aiReach * wnd.reach });
|
recordEvent('opponent_whiff', { chance: flub, chaseSpeed: rzChaseSpeed(rz), reach: TUNE.aiReach * wnd.reach });
|
||||||
|
if (swingLog()) logSwing({ result: 'THEM', reason: 'whiffed it' });
|
||||||
rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder)
|
rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -1904,7 +1926,7 @@ function meleeCooldown() {
|
|||||||
TUNE.meleeCool / (1 + Math.max(0, near - 1) * TUNE.meleeCrowd));
|
TUNE.meleeCool / (1 + Math.max(0, near - 1) * TUNE.meleeCrowd));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function keyPressed(code, lead = 0) {
|
export function keyPressed(code, lead = 0, flick = false) {
|
||||||
// THE SPRITE BENCH takes the key before anything else reads it: it is a back-room art test with no
|
// THE SPRITE BENCH takes the key before anything else reads it: it is a back-room art test with no
|
||||||
// ball and no scoring, so every other meaning a key has here is meaningless inside it.
|
// ball and no scoring, so every other meaning a key has here is meaningless inside it.
|
||||||
if (G.bench) return benchKey(code);
|
if (G.bench) return benchKey(code);
|
||||||
@@ -2003,7 +2025,7 @@ export function keyPressed(code, lead = 0) {
|
|||||||
// clock starts that far along: the timing is judged from touch-down, the ball still has to be
|
// clock starts that far along: the timing is judged from touch-down, the ball still has to be
|
||||||
// there for the contact.
|
// there for the contact.
|
||||||
P.swingT = rallyFeel() ? lead : 0; P.pose = 'swing'; P.poseT = 0; G.swung = true;
|
P.swingT = rallyFeel() ? lead : 0; P.pose = 'swing'; P.poseT = 0; G.swung = true;
|
||||||
P.tap = Math.round(lead * 1000); // ...and the log says how long the tap was (0 = a key or a flick)
|
P.tap = Math.round(lead * 1000); P.flick = flick; // ...and the log says which gesture, and how long it took to fire
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -207,7 +207,12 @@ cv.addEventListener('touchmove', e => {
|
|||||||
? { ax: Math.abs(dx) > dist * 0.38 ? Math.sign(dx) : 0,
|
? { ax: Math.abs(dx) > dist * 0.38 ? Math.sign(dx) : 0,
|
||||||
az: Math.abs(dy) > dist * 0.38 ? Math.sign(dy) : 0,
|
az: Math.abs(dy) > dist * 0.38 ? Math.sign(dy) : 0,
|
||||||
fx: dx / dist, fy: dy / dist, mag: dist } : null;
|
fx: dx / dist, fy: dy / dist, mag: dist } : null;
|
||||||
if (G.swipe && !stick.swung && G.phase === 'rally') { stick.swung = true; keyPressed('Space'); }
|
// ...timed, like the tap, from the moment the finger LANDED (gameplay.keyPressed's `lead`): the
|
||||||
|
// timed gold ring means "touch now", and a flick only swings once it has travelled flickMin.
|
||||||
|
// Capped at flickLead, so a slow drag still has a window to swing into (the left-flick lesson).
|
||||||
|
if (G.swipe && !stick.swung && G.phase === 'rally') {
|
||||||
|
stick.swung = true; keyPressed('Space', Math.min(TUNE.flickLead, (performance.now() - stick.t0) / 1000), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
@@ -235,7 +240,7 @@ const endTouch = e => {
|
|||||||
az: Math.abs(dy) > dist * 0.38 ? Math.sign(dy) : 0,
|
az: Math.abs(dy) > dist * 0.38 ? Math.sign(dy) : 0,
|
||||||
fx: dx / dist, fy: dy / dist, mag: dist, // raw vector: splatter free-aim
|
fx: dx / dist, fy: dy / dist, mag: dist, // raw vector: splatter free-aim
|
||||||
t: performance.now() };
|
t: performance.now() };
|
||||||
if (!stick.swung) keyPressed('Space'); // the crossing already swung — never twice per gesture
|
if (!stick.swung) keyPressed('Space', Math.min(TUNE.flickLead, dt / 1000), true); // the crossing already swung — never twice per gesture
|
||||||
}
|
}
|
||||||
stick = null; touchAxis.x = 0; touchAxis.z = 0; G.swipe = null;
|
stick = null; touchAxis.x = 0; touchAxis.z = 0; G.swipe = null;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -3317,7 +3317,7 @@ export function render() {
|
|||||||
ctx.fillStyle = UI.dim; ctx.fillText('SWING LOG — TAP HERE TO COPY · SCOREBOARD HIDES', 10, L.y + 5);
|
ctx.fillStyle = UI.dim; ctx.fillText('SWING LOG — TAP HERE TO COPY · SCOREBOARD HIDES', 10, L.y + 5);
|
||||||
const maxCh = Math.floor((W - 20) / 4.3);
|
const maxCh = Math.floor((W - 20) / 4.3);
|
||||||
list.forEach((e, i) => {
|
list.forEach((e, i) => {
|
||||||
ctx.fillStyle = e.result === 'HIT' || e.result === 'WON' ? '#7fd07f' : '#ff9060';
|
ctx.fillStyle = e.result === 'HIT' || e.result === 'WON' ? '#7fd07f' : e.result === 'THEM' ? UI.dim : '#ff9060';
|
||||||
ctx.fillText((e.at + 's ' + e.result + ' ' + e.reason).slice(0, maxCh), 10, L.y + 16 + i * lh);
|
ctx.fillText((e.at + 's ' + e.result + ' ' + e.reason).slice(0, maxCh), 10, L.y + 16 + i * lh);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1390,6 +1390,10 @@ test('THE SWING LOG writes in the match too: a whiff, a hit with its tap, and wh
|
|||||||
}
|
}
|
||||||
const hit = G.missLog.find(e => e.result === 'HIT');
|
const hit = G.missLog.find(e => e.result === 'HIT');
|
||||||
assert.ok(hit && /^q=/.test(hit.reason) && /tap 100ms/.test(hit.reason), 'the hit says how it was struck: ' + (hit && hit.reason));
|
assert.ok(hit && /^q=/.test(hit.reason) && /tap 100ms/.test(hit.reason), 'the hit says how it was struck: ' + (hit && hit.reason));
|
||||||
|
// ...and the opponent's half is there: what it hit back, and where it lands relative to you
|
||||||
|
for (let i = 0; i < 2 * 60 && ball.lastHit === 'P'; i++) { zb.swingCool = 0; update(DT); }
|
||||||
|
const them = G.missLog.find(e => e.result === 'THEM');
|
||||||
|
assert.ok(them && (/lands/.test(them.reason) || /whiffed/.test(them.reason)), 'the reply is logged: ' + (them && them.reason));
|
||||||
} finally { TUNE.autoAssist = assist; }
|
} finally { TUNE.autoAssist = assist; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user