Field data (miss log) showed the dominant unfair miss: the assist parks you on the landing spot, the ball spends the whole swing window below minY rising off the bounce at your feet, and becomes hittable one frame after the window closes. Root fix: the below-ankles rejection now only applies to a DROPPING ball — rising post-bounce balls can be scooped at any height (a half-volley). Feet-bounce taps go from ~0 to 19/30 winning frames; 30 simulated real points now log 50 hits, 0 whiffs. Also: swing-log snapshots record the same swing-timer value trySwing tests (removes the one-frame misclassification behind 'unclassified'), and tapping the scoreboard toggles an on-screen dump of the last 9 log entries so live situations can be screenshotted from the phone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
60 lines
2.7 KiB
JavaScript
60 lines
2.7 KiB
JavaScript
// Splatter Tennis — boot, main loop, intro overlay, and the window.T test hooks.
|
|
// World units: meters. z axis = court depth, 0 at FAR baseline, 23.77 at NEAR baseline.
|
|
// Module map: config (tuning/constants) · state (mutable game state) · gameplay (logic/update)
|
|
// render (camera/drawing) · input (keys/touch) · audio (sfx/umpire) · panel (T tuning UI)
|
|
|
|
import { TUNE, COURT, UMP_H } from './config.js';
|
|
import { P, O, ball, score, G, setupPoint } from './state.js';
|
|
import { update, keyPressed, solveShot, inServeBox, inCourt, forceZombies } from './gameplay.js';
|
|
import { fitViewport, render, proj } from './render.js';
|
|
import { voice } from './audio.js';
|
|
import { zombies, spawnZombie } from './zombies.js';
|
|
import { cv } from './dom.js';
|
|
import './input.js';
|
|
|
|
let last = performance.now();
|
|
function frame(now) {
|
|
const dt = Math.min(0.033, (now - last) / 1000); last = now;
|
|
update(dt); render();
|
|
requestAnimationFrame(frame);
|
|
}
|
|
|
|
fitViewport();
|
|
setupPoint();
|
|
|
|
// intro overlay: first tap dismisses it, unlocks audio (iOS needs the gesture) and starts play
|
|
const intro = document.getElementById('intro');
|
|
function dismissIntro() {
|
|
if (!intro.parentNode) return;
|
|
intro.remove(); voice('quiet_please');
|
|
}
|
|
intro.addEventListener('pointerdown', e => { e.preventDefault(); dismissIntro(); });
|
|
addEventListener('keydown', () => dismissIntro(), { once: true });
|
|
|
|
// eaten: any tap or key (after a beat, so death-spam doesn't skip the screen) restarts
|
|
const maybeRestart = () => { if (G.mode === 'eaten' && G.eatenT > 0.7) location.reload(); }; // ponytail: reload = honest full reset
|
|
cv.addEventListener('pointerdown', maybeRestart);
|
|
addEventListener('keydown', maybeRestart);
|
|
|
|
// hidden shortcut: tap the umpire three times to summon zombie mode early
|
|
let umpTaps = [];
|
|
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();
|
|
umpTaps = umpTaps.filter(t => now - t < 1600);
|
|
umpTaps.push(now);
|
|
if (umpTaps.length >= 3) { umpTaps = []; forceZombies(); }
|
|
});
|
|
|
|
requestAnimationFrame(frame);
|
|
|
|
// exposed for in-browser sanity checks and simulated playtests
|
|
window.T = { TUNE, ball, P, O, score, G, zombies, spawnZombie, solveShot, inServeBox, inCourt, proj, setupPoint,
|
|
step: dt => update(dt), press: keyPressed,
|
|
get phase() { return G.phase; }, set phase(v) { G.phase = v; } };
|