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,101 @@
|
||||
// All sound: WebAudio context, synthesized SFX, recorded umpire lines, speech fallback.
|
||||
|
||||
let AC = null;
|
||||
export const audio = () => {
|
||||
if (!AC) AC = new (window.AudioContext || window.webkitAudioContext)();
|
||||
if (AC.state === 'suspended') AC.resume();
|
||||
return AC;
|
||||
};
|
||||
|
||||
// Umpire lines are pre-recorded files (assets/audio/*.m4a) played via WebAudio —
|
||||
// works on mobile where speechSynthesis is unreliable. say() remains the fallback.
|
||||
const VOICE = {};
|
||||
let reverbNode = null;
|
||||
function reverb(ac) { // stadium PA tail: exponentially decaying noise as impulse response
|
||||
if (reverbNode) return reverbNode;
|
||||
const len = ac.sampleRate * 1.3, ir = ac.createBuffer(2, len, ac.sampleRate);
|
||||
for (let c = 0; c < 2; c++) {
|
||||
const d = ir.getChannelData(c);
|
||||
for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 2.8);
|
||||
}
|
||||
reverbNode = ac.createConvolver(); reverbNode.buffer = ir;
|
||||
const wet = ac.createGain(); wet.gain.value = 0.35;
|
||||
reverbNode.connect(wet).connect(ac.destination);
|
||||
return reverbNode;
|
||||
}
|
||||
|
||||
export function voice(name, delay = 0) {
|
||||
const ac = audio();
|
||||
const play = buf => {
|
||||
const src = ac.createBufferSource(); src.buffer = buf;
|
||||
src.connect(ac.destination); src.connect(reverb(ac)); // dry + center-court echo
|
||||
src.start(ac.currentTime + delay);
|
||||
};
|
||||
if (VOICE[name]) return play(VOICE[name]);
|
||||
fetch('assets/audio/' + name + '.m4a')
|
||||
.then(r => { if (!r.ok) throw 0; return r.arrayBuffer(); })
|
||||
.then(ab => ac.decodeAudioData(ab))
|
||||
.then(buf => { VOICE[name] = buf; play(buf); })
|
||||
.catch(() => say(name.replace(/_/g, ' '), delay));
|
||||
}
|
||||
|
||||
let sysVoice = null;
|
||||
export function say(text, delay = 0) { // fallback when a recorded umpire line is missing
|
||||
try {
|
||||
if (!sysVoice) sysVoice = speechSynthesis.getVoices().find(v => v.lang.startsWith('en-GB')) || null;
|
||||
const u = new SpeechSynthesisUtterance(text);
|
||||
if (sysVoice) u.voice = sysVoice; u.rate = 0.88; u.pitch = 0.75; u.volume = 0.9;
|
||||
setTimeout(() => speechSynthesis.speak(u), delay * 1000);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function noiseBuf(ac, len) {
|
||||
const b = ac.createBuffer(1, ac.sampleRate * len, ac.sampleRate), d = b.getChannelData(0);
|
||||
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
|
||||
return b;
|
||||
}
|
||||
|
||||
export function swish(vol = 0.3) { // racket cutting air — the sound of a whiff
|
||||
try {
|
||||
const ac = audio(), t = ac.currentTime;
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, 0.16);
|
||||
const bp = ac.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(1600, t);
|
||||
bp.frequency.exponentialRampToValueAtTime(3200, t + 0.12); bp.Q.value = 1.5;
|
||||
const g = ac.createGain();
|
||||
g.gain.setValueAtTime(0.001, t); g.gain.exponentialRampToValueAtTime(vol, t + 0.05);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, t + 0.16);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function pock(vol = 0.8, freq = 900) { // racket contact / bounce thock
|
||||
try {
|
||||
const ac = audio(), t = ac.currentTime;
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, 0.06);
|
||||
const bp = ac.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = freq; bp.Q.value = 1.2;
|
||||
const g = ac.createGain();
|
||||
g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.001, t + 0.07);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start();
|
||||
const o = ac.createOscillator(), og = ac.createGain(); // low thump body
|
||||
o.frequency.setValueAtTime(180, t); o.frequency.exponentialRampToValueAtTime(70, t + 0.08);
|
||||
og.gain.setValueAtTime(vol * 0.5, t); og.gain.exponentialRampToValueAtTime(0.001, t + 0.09);
|
||||
o.connect(og).connect(ac.destination); o.start(t); o.stop(t + 0.1);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function applause(seconds = 2.4, vol = 0.5) { // ponytail: synthesized claps; swap for a sample if it reads flat
|
||||
try {
|
||||
const ac = audio(), t0 = ac.currentTime, n = 90 * seconds;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = t0 + Math.random() * seconds;
|
||||
const env = Math.min(1, 4 * (1 - Math.abs(2 * (t - t0) / seconds - 1))); // fade in/out
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, 0.025);
|
||||
const bp = ac.createBiquadFilter(); bp.type = 'bandpass';
|
||||
bp.frequency.value = 1000 + Math.random() * 1600; bp.Q.value = 2.5;
|
||||
const g = ac.createGain();
|
||||
g.gain.setValueAtTime(vol * env * (0.3 + Math.random() * 0.7), t);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, t + 0.03);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start(t);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
Reference in new issue
Block a user