Split the monolith into ES modules; hoist scattered magic numbers
game.js (830 lines) -> 9 modules, native ES modules so still zero build step: dom canvas handles (imports nothing; keeps cycles harmless) config TUNE (live-tunable) + structural constants state all mutable game state (G) + setupPoint/sendBoy audio sfx synthesis, recorded umpire voice, reverb, speech fallback input keyboard + touch, exports axes and takeFlickAim() gameplay ballistics, swings, serve, rules, AI, update loop render camera, viewport, sprites, crowd, all drawing panel T tuning UI main boot, loop, intro, window.T test hooks Deduped into named constants: bounce tempo (was in 3 places), contact height band, intercept zone depth, home positions, player bounds, toss params, shot depths, pose hold time, sprite scale, actor heights. Also extracted drawShadow/drawBall/expirePose from repeated code. One intentional import cycle (input <-> gameplay), safe: both sides only call functions at runtime, never during module evaluation. Regression-verified in browser sims: point cycle, CPU returns 11/12, assist contract 8/8, corner-winner gradient intact (0/12 perfect corners returned, 10/12 center balls returned). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2bf21f252
commit
c506a631c2
12 files changed
+936
-824
No files matched your search
@@ -0,0 +1,75 @@
|
||||
// Keyboard + one-handed touch. Exposes movement axes and the flick-aim for gameplay.
|
||||
// Note: input <-> gameplay is an intentional import cycle; both only call each other's
|
||||
// functions at runtime, never during module evaluation.
|
||||
|
||||
import { cv } from './dom.js';
|
||||
import { audio } from './audio.js';
|
||||
import { keyPressed } from './gameplay.js';
|
||||
import { togglePanel } from './panel.js';
|
||||
|
||||
const keys = {};
|
||||
addEventListener('keydown', e => {
|
||||
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) e.preventDefault();
|
||||
if (e.key === 't' || e.key === 'T') togglePanel();
|
||||
if (!keys[e.code]) keyPressed(e.code);
|
||||
keys[e.code] = true;
|
||||
});
|
||||
addEventListener('keyup', e => keys[e.code] = false);
|
||||
|
||||
const touchAxis = { x: 0, z: 0 };
|
||||
export const axisX = () => touchAxis.x || (keys.ArrowRight || keys.KeyD ? 1 : 0) - (keys.ArrowLeft || keys.KeyA ? 1 : 0);
|
||||
export const axisZ = () => touchAxis.z || (keys.ArrowUp || keys.KeyW ? -1 : 0) + (keys.ArrowDown || keys.KeyS ? 1 : 0);
|
||||
|
||||
// One-handed touch: hold-drag = run, quick FLICK = swing aimed in the flick direction,
|
||||
// plain tap = neutral swing / toss / serve. Extra finger tap still swings (two-thumb play).
|
||||
let stick = null; // {id, x0, y0, t0, moved}
|
||||
let flickAim = null; // {ax, az, t} aim override for the swing just triggered
|
||||
const DEAD = 14;
|
||||
|
||||
// gameplay consumes the flick at contact; returns null if none or expired
|
||||
export function takeFlickAim(maxAgeMs = 450) {
|
||||
if (flickAim && performance.now() - flickAim.t < maxAgeMs) {
|
||||
const aim = { ax: flickAim.ax, az: flickAim.az };
|
||||
flickAim = null;
|
||||
return aim;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
cv.addEventListener('touchstart', e => {
|
||||
e.preventDefault(); audio();
|
||||
for (const t of e.changedTouches) {
|
||||
if (!stick) stick = { id: t.identifier, x0: t.clientX, y0: t.clientY, t0: performance.now(), moved: false };
|
||||
else keyPressed('Space');
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
cv.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
if (!stick) return;
|
||||
for (const t of e.changedTouches) {
|
||||
if (t.identifier !== stick.id) continue;
|
||||
const dx = t.clientX - stick.x0, dy = t.clientY - stick.y0;
|
||||
if (Math.hypot(dx, dy) > DEAD) stick.moved = true;
|
||||
touchAxis.x = Math.abs(dx) > DEAD ? Math.sign(dx) : 0;
|
||||
touchAxis.z = Math.abs(dy) > DEAD ? Math.sign(dy) : 0;
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
const endTouch = e => {
|
||||
e.preventDefault();
|
||||
for (const t of e.changedTouches) {
|
||||
if (!stick || t.identifier !== stick.id) continue;
|
||||
const dt = performance.now() - stick.t0;
|
||||
const dx = t.clientX - stick.x0, dy = t.clientY - stick.y0, dist = Math.hypot(dx, dy);
|
||||
if (!stick.moved && dt < 250) keyPressed('Space'); // tap
|
||||
else if (dt < 300 && dist > 28) { // flick = aimed swing
|
||||
flickAim = { ax: Math.abs(dx) > dist * 0.38 ? Math.sign(dx) : 0,
|
||||
az: Math.abs(dy) > dist * 0.38 ? Math.sign(dy) : 0, t: performance.now() };
|
||||
keyPressed('Space');
|
||||
}
|
||||
stick = null; touchAxis.x = 0; touchAxis.z = 0;
|
||||
}
|
||||
};
|
||||
cv.addEventListener('touchend', endTouch, { passive: false });
|
||||
cv.addEventListener('touchcancel', endTouch, { passive: false });
|
||||
Reference in new issue
Block a user