5 dogs with distinct movement physics, seeded procedural arenas with traps and mineable powerups, smash-style knockback/stocks combat, local 2P and P2P netplay (PeerJS room keys), procedural art and sfx, headless sim smoke tests.
105 lines
4.8 KiB
JavaScript
105 lines
4.8 KiB
JavaScript
// Procedural sound effects — pure WebAudio synthesis, no asset files.
|
|
let ac = null, master = null, muted = false;
|
|
|
|
export function initAudio() {
|
|
if (!ac) {
|
|
const AC = window.AudioContext || window.webkitAudioContext;
|
|
if (!AC) return;
|
|
ac = new AC();
|
|
master = ac.createGain();
|
|
master.gain.value = 0.35;
|
|
master.connect(ac.destination);
|
|
}
|
|
if (ac.state === 'suspended') ac.resume();
|
|
}
|
|
|
|
export function toggleMute() { muted = !muted; return muted; }
|
|
|
|
function tone(freq, dur, type = 'square', vol = 0.18, slide = 0) {
|
|
const t = ac.currentTime;
|
|
const o = ac.createOscillator(), g = ac.createGain();
|
|
o.type = type;
|
|
o.frequency.setValueAtTime(freq, t);
|
|
if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(30, freq + slide), t + dur);
|
|
g.gain.setValueAtTime(vol, t);
|
|
g.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
|
o.connect(g); g.connect(master);
|
|
o.start(t); o.stop(t + dur + 0.02);
|
|
}
|
|
|
|
function noise(dur, vol = 0.25, filterFreq = 1200, q = 1) {
|
|
const t = ac.currentTime;
|
|
const len = Math.max(1, Math.floor(ac.sampleRate * dur));
|
|
const buf = ac.createBuffer(1, len, ac.sampleRate);
|
|
const d = buf.getChannelData(0);
|
|
for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / len);
|
|
const src = ac.createBufferSource();
|
|
src.buffer = buf;
|
|
const f = ac.createBiquadFilter();
|
|
f.type = 'lowpass'; f.frequency.value = filterFreq; f.Q.value = q;
|
|
const g = ac.createGain();
|
|
g.gain.setValueAtTime(vol, t);
|
|
g.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
|
src.connect(f); f.connect(g); g.connect(master);
|
|
src.start(t);
|
|
}
|
|
|
|
export function sfx(name) {
|
|
if (!ac || muted) return;
|
|
switch (name) {
|
|
case 'jump': tone(300, 0.12, 'square', 0.12, 250); break;
|
|
case 'airjump': tone(380, 0.1, 'square', 0.1, 280); break;
|
|
case 'land': noise(0.08, 0.12, 500); break;
|
|
case 'bark': tone(170, 0.07, 'sawtooth', 0.2, -60); tone(340, 0.05, 'square', 0.08, -100); break;
|
|
case 'hit': noise(0.12, 0.3, 900); tone(110, 0.15, 'triangle', 0.25, -50); break;
|
|
case 'block': tone(520, 0.1, 'sine', 0.15, 100); break;
|
|
case 'dash': noise(0.1, 0.12, 2500); tone(600, 0.08, 'sine', 0.06, 400); break;
|
|
case 'spike': tone(200, 0.2, 'sawtooth', 0.2, -140); break;
|
|
case 'saw': noise(0.15, 0.25, 1800, 4); break;
|
|
case 'pad': tone(180, 0.22, 'sine', 0.22, 380); break;
|
|
case 'mine': tone(900 + Math.random() * 300, 0.05, 'square', 0.1); break;
|
|
case 'break': tone(700, 0.07, 'square', 0.12, 300); tone(1050, 0.12, 'square', 0.1, 400); break;
|
|
case 'pickup': tone(660, 0.07, 'square', 0.12); tone(990, 0.1, 'square', 0.1); break;
|
|
case 'throw': noise(0.06, 0.1, 3000); break;
|
|
case 'boom': noise(0.5, 0.4, 350, 0.5); tone(60, 0.4, 'sine', 0.3, -25); break;
|
|
case 'shield': tone(440, 0.25, 'sine', 0.15, 220); break;
|
|
case 'boost': tone(330, 0.1, 'square', 0.12, 200); tone(500, 0.12, 'square', 0.1, 300); break;
|
|
case 'heal': tone(520, 0.12, 'sine', 0.12, 160); tone(780, 0.15, 'sine', 0.1, 100); break;
|
|
case 'grapple': tone(800, 0.06, 'square', 0.12, -300); break;
|
|
case 'whiff': tone(250, 0.08, 'sine', 0.08, -100); break;
|
|
case 'blink': tone(1000, 0.12, 'sine', 0.1, 500); break;
|
|
case 'jet': noise(0.07, 0.07, 700); break;
|
|
case 'boing': tone(140, 0.25, 'sine', 0.22, 320); break;
|
|
case 'charge': tone(120, 0.2, 'sawtooth', 0.18, 80); break;
|
|
case 'thud': noise(0.1, 0.25, 400); break;
|
|
case 'pound': noise(0.25, 0.35, 500); tone(70, 0.3, 'sine', 0.3, -20); break;
|
|
case 'ko': tone(500, 0.5, 'sawtooth', 0.25, -420); noise(0.4, 0.3, 800); break;
|
|
case 'respawn': tone(400, 0.15, 'sine', 0.12, 300); break;
|
|
case 'go': tone(523, 0.12, 'square', 0.15); tone(784, 0.25, 'square', 0.15); break;
|
|
case 'count': tone(440, 0.1, 'square', 0.12); break;
|
|
case 'over': tone(523, 0.15, 'square', 0.15); tone(659, 0.15, 'square', 0.15); tone(784, 0.3, 'square', 0.15); break;
|
|
case 'drop': tone(880, 0.1, 'sine', 0.08, -200); break;
|
|
case 'ui': tone(600, 0.05, 'square', 0.08); break;
|
|
}
|
|
}
|
|
|
|
// Map sim events → sfx, with a per-frame cap so explosions don't clip.
|
|
const EVENT_SFX = {
|
|
jump: 'jump', land: 'land', bark: 'bark', hit: 'hit', block: 'block',
|
|
dash: 'dash', spike: 'spike', saw: 'saw', pad: 'pad', crack: 'mine',
|
|
tile: 'break', pickup: 'pickup', throw: 'throw', boom: 'boom',
|
|
shield: 'shield', boost: 'boost', heal: 'heal', grapple: 'grapple',
|
|
whiff: 'whiff', blink: 'blink', jet: 'jet', boing: 'boing',
|
|
charge: 'charge', thud: 'thud', pound: 'pound', ko: 'ko',
|
|
respawn: 'respawn', go: 'go', over: 'over', drop: 'drop',
|
|
};
|
|
|
|
export function playEvents(events) {
|
|
let n = 0;
|
|
for (const ev of events) {
|
|
if (n >= 8) break;
|
|
let name = EVENT_SFX[ev.t];
|
|
if (ev.t === 'jump' && ev.air) name = 'airjump';
|
|
if (name) { sfx(name); n++; }
|
|
}
|
|
}
|