diff --git a/gameplay.js b/gameplay.js index 670a78c..89917cb 100644 --- a/gameplay.js +++ b/gameplay.js @@ -331,7 +331,7 @@ function trySwing(ch, isAI) { if (ball.lastHit === ch.side) return; 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 (ball.y < CONTACT.minY) return; // below the ankles the ball is past you — no grass-scraping saves + 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 // quality: how centered in the window the contact lands const center = TUNE.swingWindup + win / 2; const off = Math.abs(t - center) / (win / 2); @@ -430,13 +430,14 @@ export function update(dt) { else if (Math.abs(assist) > 0.25) P.facing = assist > 0 ? 1 : -1; // hysteresis: small corrections don't flip facing } if (P.swingT >= 0) { - if (G.mode === 'tennis' && ball.active && ball.lastHit === 'O') { // sample the approach for the miss log + P.swingT += dt; + if (G.mode === 'tennis' && ball.active && ball.lastHit === 'O') { // sample the approach for the miss log (t = what trySwing tests this frame) const volley = ball.bounces === 0; (G.swingSnaps ||= []).push({ t: P.swingT, d: Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8), y: ball.y, bz: ball.z, bounces: ball.bounces, r: TUNE.reach * (volley ? VOLLEY.reach : 1), w: TUNE.swingWindow * (volley ? VOLLEY.window : 1) }); } - P.swingT += dt; trySwing(P, false); // a contact resets swingT to -1 inside trySwing + trySwing(P, false); // a contact resets swingT to -1 inside trySwing const win = TUNE.swingWindow * (ball.active && ball.bounces === 0 ? VOLLEY.window : 1); // match trySwing's volley window if (P.swingT > TUNE.swingWindup + win + 0.02) { P.swingT = -1; if (G.phase === 'rally') swish(); logWhiff(); } // log even if the point ended mid-swing } diff --git a/main.js b/main.js index 54cddc2..ab2eb08 100644 --- a/main.js +++ b/main.js @@ -42,6 +42,7 @@ cv.addEventListener('pointerdown', e => { const r = cv.getBoundingClientRect(); const x = (e.clientX - r.left) / r.width * (cv.width / 2); // logical coords (RES = 2) const y = (e.clientY - r.top) / r.height * (cv.height / 2); + if (x >= 6 && x <= 138 && y >= 6 && y <= 36) { G.logShow = !G.logShow; return; } // scoreboard tap = miss-log overlay const u = proj(7.1, 0, COURT.netZ); if (Math.abs(x - u.x) > 16 || y > u.y + 6 || y < u.y - UMP_H * u.s - 8) return; const now = performance.now(); diff --git a/render.js b/render.js index cd6be7a..a509adf 100644 --- a/render.js +++ b/render.js @@ -294,5 +294,16 @@ export function render() { ctx.font = 'bold 8px monospace'; ctx.fillStyle = '#ff9060'; ctx.fillText(G.lastMiss.text.toUpperCase(), W / 2, H - 28); } + if (G.logShow) { // scoreboard tap: recent swing diagnostics, screenshot-friendly + const list = G.missLog.slice(-9), lh = 10, top = 40; + ctx.fillStyle = 'rgba(8,10,16,0.88)'; ctx.fillRect(6, top, W - 12, list.length * lh + 16); + ctx.font = 'bold 7px monospace'; ctx.textAlign = 'left'; + ctx.fillStyle = '#8fa0b8'; ctx.fillText('SWING LOG — tap scoreboard to hide', 10, top + 5); + const maxCh = Math.floor((W - 20) / 4.3); + list.forEach((e, i) => { + ctx.fillStyle = e.result === 'HIT' ? '#7fd07f' : '#ff9060'; + ctx.fillText((e.at + 's ' + e.result + ' ' + e.reason).slice(0, maxCh), 10, top + 16 + i * lh); + }); + } ctx.textAlign = 'left'; } diff --git a/state.js b/state.js index 8275aa1..5768334 100644 --- a/state.js +++ b/state.js @@ -35,6 +35,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 + logShow: false, // tap the scoreboard to toggle the on-screen miss-log dump }; export const other = s => s === 'P' ? 'O' : 'P';