Sound: recorded SFX layer + a touch-easier CPU
Wire the new assets/audio/*.wav samples over the synth SFX: - Ball hits: per-side tennis-player / tennis-opponent on serve & return, now with a stadium reverb tail (same convolver as umpire/aww). - Crowd: tennis-applaus on your points/games (louder on big moments); tennis-aww when the opponent scores or you're eaten (reverb tail). - Ambient bed: tennis-ambient-loop from first tap, 500Hz lowcut, low vol. - Zombie music: zombie-mode loop when splatter kicks in (800Hz lowcut), fades out for the 1:1 sudden-death duel and back in after. - Headshots: random headshot-1/-2 on every zombie kill (killZombie). audio.js: sfx() one-shot player w/ rate+reverb opts, loopBed() beds with duck/fade, aww() wrapper, hotter reverb send on the umpire voice. Also: normal opponent aiFlub 0.09 -> 0.14 (a little easier to beat). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
88e46a62b8
commit
eacad1ffca
13 files changed
+75
-13
No files matched your search
@@ -29,7 +29,9 @@ export function voice(name, delay = 0, rate = 1) {
|
||||
const play = buf => {
|
||||
const src = ac.createBufferSource(); src.buffer = buf;
|
||||
src.playbackRate.value = rate; // < 1 = slower and lower — the umpire's dark register
|
||||
src.connect(ac.destination); src.connect(reverb(ac)); // dry + center-court echo
|
||||
src.connect(ac.destination); // dry
|
||||
const send = ac.createGain(); send.gain.value = 1.8; // umpire rides a wetter PA tail than the crowd sfx
|
||||
src.connect(send).connect(reverb(ac)); // extra reverb send — the big-stadium announcer echo
|
||||
src.start(ac.currentTime + delay);
|
||||
};
|
||||
if (VOICE[name]) return play(VOICE[name]);
|
||||
@@ -40,6 +42,62 @@ export function voice(name, delay = 0, rate = 1) {
|
||||
.catch(() => say(name.replace(/_/g, ' '), delay));
|
||||
}
|
||||
|
||||
// Recorded one-shot SFX (assets/audio/tennis-*.wav): ball hits, applause, the crowd's aww.
|
||||
// Same load-once-then-cache pattern as voice(), but dry (no PA reverb) and with a volume knob.
|
||||
const SFX = {};
|
||||
export function sfx(name, vol = 1, opts = {}) {
|
||||
const ac = audio();
|
||||
const play = buf => {
|
||||
const src = ac.createBufferSource(); src.buffer = buf;
|
||||
if (opts.rate) src.playbackRate.value = opts.rate; // < 1 stretches it a touch longer (and lower)
|
||||
const g = ac.createGain(); g.gain.value = vol;
|
||||
src.connect(g).connect(ac.destination);
|
||||
if (opts.reverb) g.connect(reverb(ac)); // wet stadium tail on top of the dry sample
|
||||
src.start();
|
||||
};
|
||||
if (SFX[name]) return play(SFX[name]);
|
||||
fetch('assets/audio/' + name + '.wav')
|
||||
.then(r => { if (!r.ok) throw 0; return r.arrayBuffer(); })
|
||||
.then(ab => ac.decodeAudioData(ab))
|
||||
.then(buf => { SFX[name] = buf; play(buf); })
|
||||
.catch(() => {}); // missing sample = silence, never a thrown error mid-rally
|
||||
}
|
||||
|
||||
// the crowd's groan — stretched a touch and pushed through the PA reverb for a longer, wetter sigh
|
||||
export function aww(vol = 1) { sfx('tennis-aww', vol * 0.5, { reverb: true }); }
|
||||
|
||||
// Looping background beds (scene atmosphere, mode music): each starts once, highpass 'lowcut' + quiet gain.
|
||||
const beds = new Map(); // file -> {ac, g, vol} once playing (null while loading); see fadeBed
|
||||
function loopBed(file, lowcut, vol) {
|
||||
if (beds.has(file)) return;
|
||||
beds.set(file, null);
|
||||
const ac = audio();
|
||||
fetch('assets/audio/' + file + '.wav')
|
||||
.then(r => { if (!r.ok) throw 0; return r.arrayBuffer(); })
|
||||
.then(ab => ac.decodeAudioData(ab))
|
||||
.then(buf => {
|
||||
const src = ac.createBufferSource(); src.buffer = buf; src.loop = true;
|
||||
const hp = ac.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = lowcut;
|
||||
const g = ac.createGain(); g.gain.value = vol;
|
||||
src.connect(hp).connect(g).connect(ac.destination); src.start();
|
||||
beds.set(file, { ac, g, vol });
|
||||
})
|
||||
.catch(() => beds.delete(file)); // failed start (decode/network) can retry next call
|
||||
}
|
||||
export const ambient = () => loopBed('tennis-ambient-loop', 500, 0.035); // whole-scene atmosphere, from first tap
|
||||
export const zombieMusic = () => loopBed('zombie-mode', 800, 0.2); // background music once splatter kicks in
|
||||
|
||||
// duck a bed to silence (out=true) or back to its base level over `secs` — the sudden-death duel mutes the music
|
||||
function fadeBed(file, out, secs = 0.7) {
|
||||
const b = beds.get(file);
|
||||
if (!b) return;
|
||||
const t = b.ac.currentTime;
|
||||
b.g.gain.cancelScheduledValues(t);
|
||||
b.g.gain.setValueAtTime(b.g.gain.value, t); // anchor at current level so a mid-fade toggle stays smooth
|
||||
b.g.gain.linearRampToValueAtTime(out ? 0 : b.vol, t + secs);
|
||||
}
|
||||
export const zombieDuck = out => fadeBed('zombie-mode', out);
|
||||
|
||||
let sysVoice = null;
|
||||
export function say(text, delay = 0) { // fallback when a recorded umpire line is missing
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user