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>
This commit is contained in:
+230
@@ -0,0 +1,230 @@
|
||||
// ═══ 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);
|
||||
}
|
||||
Reference in New Issue
Block a user