diff --git a/assets/_sheet_boyrun.png b/assets/_sheet_boyrun.png index 1db5390..b9eddc4 100644 Binary files a/assets/_sheet_boyrun.png and b/assets/_sheet_boyrun.png differ diff --git a/assets/x_boyrun1.png b/assets/x_boyrun1.png index d175713..bc71548 100644 Binary files a/assets/x_boyrun1.png and b/assets/x_boyrun1.png differ diff --git a/assets/x_boyrun2.png b/assets/x_boyrun2.png index 0bd7f87..d4404fe 100644 Binary files a/assets/x_boyrun2.png and b/assets/x_boyrun2.png differ diff --git a/game.js b/game.js index 5530da2..b41c436 100644 --- a/game.js +++ b/game.js @@ -18,7 +18,7 @@ const TUNE = { aimError: 2.4, // m of scatter at quality 0 perfectMs: 45, // |offset from window center| for PERFECT aiQualityMin: 0.55, aiQualityMax: 0.95, aiFlub: 0.05, - autoAssist: matchMedia('(pointer: coarse)').matches ? 1.0 : 0, // auto-run toward the ball (mobile) + autoAssist: matchMedia('(pointer: coarse)').matches ? 1.15 : 0, // auto-run speed factor (mobile); slightly superhuman legs }; const COURT = { L: 23.77, W: 8.23, dblW: 10.97, netZ: 11.885, netH: 0.914, netW: 10.06, serviceD: 6.4 }; @@ -367,24 +367,29 @@ function onBounce() { let phaseWasServe = false; // ---------------------------------------------------------------- update ---- -// Where should a player run to? Simulate the ball forward and take the first point -// in their half where the ball is at comfortable hitting height. +// Where should a player run to? Simulate the ball forward and take the first point in +// their half at hitting height that they can actually REACH IN TIME; otherwise chase the +// deepest hittable point. This is the "every ball is catchable, timing is the skill" contract. function intercept(side) { + const ch = side === 'O' ? O : P; + const spd = side === 'O' ? TUNE.aiSpeed : TUNE.playerSpeed * TUNE.autoAssist; let { x, y, z, vx, vy, vz } = ball; const g = TUNE.gravity, dt = 0.01; const inZone = side === 'O' ? zz => zz <= 9.5 : zz => zz >= COURT.L - 9.5; const home = side === 'O' ? { x: 0, z: 2.5 } : { x: 0, z: 22.8 }; - let fallback = null; - for (let i = 0; i < 240; i++) { + let fallback = null, deepest = null; + for (let i = 0; i < 300; i++) { vy -= g * dt; x += vx * dt; y += vy * dt; z += vz * dt; if (y <= 0 && vy < 0) { y = 0; vy = -vy * TUNE.bounceY; vx *= TUNE.bounceXZ; vz *= TUNE.bounceXZ; } - if (i > 5 && inZone(z) && y < 1.6) { + if (i > 5 && inZone(z) && y < 1.6 && y > 0.3) { const pt = { x, z: side === 'O' ? Math.max(0.8, z) : Math.min(24.5, z) }; - if (y > 0.7) return pt; // waist-high contact preferred - if (y > 0.3 && !fallback) fallback = pt; + const reachable = Math.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt; + if (y > 0.7 && reachable) return pt; // waist-high AND arrivable in time + if (reachable && !fallback) fallback = pt; // low but arrivable + deepest = pt; // last resort: run at the deepest hittable point } } - return fallback || home; + return fallback || deepest || home; } function trySwing(ch, isAI) { @@ -448,7 +453,8 @@ function update(dt) { let assist = 0; if (!mx && !mz && TUNE.autoAssist > 0 && ball.active) { const chasing = ball.lastHit === 'O'; - const rate = TUNE.playerSpeed * TUNE.autoAssist * slow * dt * (chasing ? 1 : 0.55); + // no `slow` here: tapping must not freeze your legs, or anticipating a shot loses it + const rate = TUNE.playerSpeed * TUNE.autoAssist * dt * (chasing ? 1 : 0.55); const tgt = chasing ? intercept('P') : { x: 0, z: 22.8 }; // run to the ball, then recover home const dx = tgt.x - P.x, dz = tgt.z - P.z, d = Math.hypot(dx, dz); if (d > 0.35) { // deadzone: stand ready near the spot instead of dithering over it