Files
siege-protocol/js/audio.js
T
44r0n7 622a9fd170 Initial commit: Siege Protocol
Inverted tower-defense browser game — deploy enemies yourself, tower auto-kills them, pocket credits, upgrade weapons. HTML + Canvas + vanilla JS, no build step.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-16 11:36:53 -04:00

231 lines
6.8 KiB
JavaScript

// ═══ audio.js ═══
// ============================================================
// AUDIO.JS — Web Audio API sound effects
// ============================================================
let _audioCtx = null;
let _activeNodes = 0;
const _MAX_NODES = 12;
function getAudioCtx() {
if (!_audioCtx) {
try { _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
catch (e) { _audioCtx = null; }
}
// Resume if suspended (browser autoplay policy)
if (_audioCtx && _audioCtx.state === 'suspended') _audioCtx.resume();
return _audioCtx;
}
// ── PRIMITIVE SYNTH ───────────────────────────────────────────
// type: 'sine'|'square'|'sawtooth'|'triangle'
function playTone(freq, type, gainPeak, duration, opts = {}) {
const ctx = getAudioCtx();
if (!ctx) return;
if (_activeNodes >= _MAX_NODES) return;
_activeNodes++;
const now = ctx.currentTime;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
// Optional frequency sweep
osc.type = type;
osc.frequency.setValueAtTime(freq, now);
if (opts.freqEnd !== undefined) {
osc.frequency.linearRampToValueAtTime(opts.freqEnd, now + duration);
}
const attack = opts.attack ?? 0.005;
const decay = opts.decay ?? duration * 0.3;
gain.gain.setValueAtTime(0, now);
gain.gain.linearRampToValueAtTime(gainPeak, now + attack);
gain.gain.exponentialRampToValueAtTime(0.001, now + duration);
// Optional filter
if (opts.filterFreq) {
const filter = ctx.createBiquadFilter();
filter.type = opts.filterType ?? 'lowpass';
filter.frequency.setValueAtTime(opts.filterFreq, now);
if (opts.filterEnd) filter.frequency.linearRampToValueAtTime(opts.filterEnd, now + duration);
osc.connect(filter);
filter.connect(gain);
} else {
osc.connect(gain);
}
gain.connect(ctx.destination);
osc.onended = () => { _activeNodes--; };
osc.start(now);
osc.stop(now + duration + 0.01);
}
function playNoise(gainPeak, duration, opts = {}) {
const ctx = getAudioCtx();
if (!ctx) return;
if (_activeNodes >= _MAX_NODES) return;
_activeNodes++;
const now = ctx.currentTime;
const bufLen = Math.ceil(ctx.sampleRate * duration);
const buf = ctx.createBuffer(1, bufLen, ctx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < bufLen; i++) data[i] = Math.random() * 2 - 1;
const src = ctx.createBufferSource();
src.buffer = buf;
const gain = ctx.createGain();
gain.gain.setValueAtTime(gainPeak, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + duration);
if (opts.filterFreq) {
const filter = ctx.createBiquadFilter();
filter.type = opts.filterType ?? 'bandpass';
filter.frequency.setValueAtTime(opts.filterFreq, now);
filter.Q.setValueAtTime(opts.Q ?? 1, now);
src.connect(filter);
filter.connect(gain);
} else {
src.connect(gain);
}
gain.connect(ctx.destination);
src.onended = () => { _activeNodes--; };
src.start(now);
}
// ── SOUND EFFECTS ─────────────────────────────────────────────
function sfx_cannon() {
// Deep thud + sharp crack
playNoise(0.4, 0.12, { filterFreq: 180, filterType: 'lowpass', Q: 0.8 });
playTone(120, 'sawtooth', 0.3, 0.08, { freqEnd: 40 });
}
function sfx_flamethrower() {
// Hissy noise burst
playNoise(0.25, 0.09, { filterFreq: 2200, filterType: 'bandpass', Q: 0.5 });
}
function sfx_lightning() {
// Sharp electric snap
playNoise(0.5, 0.06, { filterFreq: 4000, filterType: 'highpass' });
playTone(800, 'sawtooth', 0.2, 0.05, { freqEnd: 200 });
}
function sfx_mortar_launch() {
// Low thump with whistle
playTone(80, 'sine', 0.35, 0.15, { freqEnd: 300 });
playNoise(0.2, 0.15, { filterFreq: 300, filterType: 'lowpass' });
}
function sfx_mortar_explode() {
// Big boom
playNoise(0.7, 0.3, { filterFreq: 250, filterType: 'lowpass', Q: 0.5 });
playTone(60, 'sine', 0.5, 0.25, { freqEnd: 20 });
}
function sfx_laser() {
// High-pitched zap
playTone(1200, 'sine', 0.15, 0.05, { freqEnd: 800 });
}
function sfx_freeze() {
// Icy crystalline sound
playTone(1800, 'sine', 0.2, 0.2, { freqEnd: 600, attack: 0.02 });
playTone(2200, 'triangle', 0.1, 0.15, { freqEnd: 900 });
}
function sfx_void() {
// Deep rumbling whoosh
playTone(55, 'sawtooth', 0.3, 0.4, { freqEnd: 40, filterFreq: 200, filterEnd: 80 });
playNoise(0.15, 0.4, { filterFreq: 120, filterType: 'lowpass' });
}
function sfx_missile_launch() {
// Whoosh
playNoise(0.3, 0.18, { filterFreq: 800, filterType: 'bandpass', Q: 0.6 });
playTone(300, 'sawtooth', 0.2, 0.18, { freqEnd: 100 });
}
function sfx_arcane() {
// Magical chime
playTone(1400, 'sine', 0.15, 0.07, { freqEnd: 700 });
}
function sfx_poison_launch() {
// Bubbly squirt
playTone(400, 'sine', 0.2, 0.1, { freqEnd: 150 });
playNoise(0.1, 0.1, { filterFreq: 600, filterType: 'bandpass' });
}
function sfx_enemy_die() {
// Short crunch
playNoise(0.3, 0.08, { filterFreq: 600, filterType: 'bandpass', Q: 1.5 });
playTone(200, 'square', 0.15, 0.06, { freqEnd: 80 });
}
function sfx_enemy_breach() {
// Alarm-like hit
playTone(220, 'sawtooth', 0.5, 0.3, { freqEnd: 110 });
playNoise(0.4, 0.2, { filterFreq: 400, filterType: 'lowpass' });
}
function sfx_buy() {
// Satisfying purchase chime
playTone(600, 'sine', 0.2, 0.08);
playTone(800, 'sine', 0.15, 0.08, { attack: 0.04 });
}
function sfx_refund() {
// Reverse chime
playTone(800, 'sine', 0.15, 0.08);
playTone(600, 'sine', 0.2, 0.08, { attack: 0.04 });
}
function sfx_error() {
// Low buzzer
playTone(180, 'square', 0.2, 0.12, { freqEnd: 160 });
}
function sfx_portal_open() {
// Whooshy portal open
playTone(300, 'sine', 0.15, 0.3, { freqEnd: 900, attack: 0.1 });
playNoise(0.08, 0.3, { filterFreq: 1200, filterType: 'bandpass' });
}
function sfx_tower_damage() {
// Heavy metallic impact
playNoise(0.5, 0.18, { filterFreq: 350, filterType: 'lowpass' });
playTone(100, 'sawtooth', 0.35, 0.15, { freqEnd: 50 });
}
// Map weapon def id → fire sfx
const WEAPON_SFX = {
cannon: sfx_cannon,
flamethrower: sfx_flamethrower,
chainlightning: sfx_lightning,
mortar: sfx_mortar_launch,
laser: sfx_laser,
freezebomb: sfx_freeze,
voidcannon: sfx_void,
missilepod: sfx_missile_launch,
arcaneturret: sfx_arcane,
poisoncloud: sfx_poison_launch,
};
function sfx_weapon_fire(defId) {
const fn = WEAPON_SFX[defId];
if (fn) fn();
}
// Throttle repeated sounds (e.g. flamethrower fires every 8 frames)
const _sfxThrottle = {};
function sfx_weapon_fire_throttled(defId, minInterval = 100) {
const now = Date.now();
if (_sfxThrottle[defId] && now - _sfxThrottle[defId] < minInterval) return;
_sfxThrottle[defId] = now;
sfx_weapon_fire(defId);
}