declare global { interface Window { webkitAudioContext?: typeof AudioContext; } } let audioCtx: AudioContext | null = null; function getCtx(): AudioContext | null { if (typeof window === "undefined") return null; const AudioCtor = window.AudioContext ?? window.webkitAudioContext; if (!AudioCtor) return null; if (!audioCtx) audioCtx = new AudioCtor(); if (audioCtx.state === "suspended") void audioCtx.resume(); return audioCtx; } type ToneOptions = { readonly frequency: number; readonly endFrequency?: number; readonly duration: number; readonly type?: OscillatorType; readonly volume?: number; readonly delay?: number; }; function playTone(ctx: AudioContext, options: ToneOptions): void { const { frequency, endFrequency, duration, type = "sine", volume = 0.2, delay = 0 } = options; const oscillator = ctx.createOscillator(); const gain = ctx.createGain(); const startAt = ctx.currentTime + delay; oscillator.type = type; oscillator.frequency.setValueAtTime(Math.max(frequency, 1), startAt); if (endFrequency !== undefined && endFrequency !== frequency) { oscillator.frequency.exponentialRampToValueAtTime( Math.max(endFrequency, 1), startAt + duration, ); } gain.gain.setValueAtTime(0.0001, startAt); gain.gain.exponentialRampToValueAtTime(volume, startAt + 0.012); gain.gain.exponentialRampToValueAtTime(0.0001, startAt + duration); oscillator.connect(gain); gain.connect(ctx.destination); oscillator.start(startAt); oscillator.stop(startAt + duration + 0.05); } function playNoise( ctx: AudioContext, options: { readonly duration: number; readonly volume?: number; readonly lowpass?: number }, ): void { const { duration, volume = 0.2, lowpass = 800 } = options; const bufferSize = Math.max(1, Math.floor(ctx.sampleRate * duration)); const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data = buffer.getChannelData(0); for (let index = 0; index < bufferSize; index += 1) { data[index] = Math.random() * 2 - 1; } const source = ctx.createBufferSource(); source.buffer = buffer; const filter = ctx.createBiquadFilter(); filter.type = "lowpass"; filter.frequency.setValueAtTime(lowpass, ctx.currentTime); const gain = ctx.createGain(); gain.gain.setValueAtTime(0.0001, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(volume, ctx.currentTime + 0.005); gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + duration); source.connect(filter); filter.connect(gain); gain.connect(ctx.destination); source.start(); } export function playFire(): void { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 180, endFrequency: 90, duration: 0.055, type: "square", volume: 0.12, }); } export function playHeadshot(): void { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 720, endFrequency: 1180, duration: 0.12, type: "triangle", volume: 0.16, }); playTone(ctx, { frequency: 1040, duration: 0.16, type: "sine", volume: 0.12, delay: 0.06 }); } export function playTorsoHit(): void { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 360, endFrequency: 220, duration: 0.1, type: "triangle", volume: 0.15, }); } export function playCivilianHit(): void { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 300, endFrequency: 140, duration: 0.22, type: "sawtooth", volume: 0.16, }); playNoise(ctx, { duration: 0.12, volume: 0.12, lowpass: 520 }); } export function playMiss(): void { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 120, endFrequency: 55, duration: 0.18, type: "sawtooth", volume: 0.14, }); playNoise(ctx, { duration: 0.11, volume: 0.11, lowpass: 480 }); } export function playCountdownTick(critical = false): void { const ctx = getCtx(); if (!ctx) return; const frequency = critical ? 880 : 520; playTone(ctx, { frequency, duration: critical ? 0.09 : 0.06, type: "square", volume: 0.1 }); } export function playRoundComplete(): void { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 420, duration: 0.12, type: "triangle", volume: 0.14 }); playTone(ctx, { frequency: 630, duration: 0.12, type: "triangle", volume: 0.14, delay: 0.1 }); playTone(ctx, { frequency: 940, duration: 0.24, type: "sine", volume: 0.16, delay: 0.2 }); } export function playXp(): void { const ctx = getCtx(); if (!ctx) return; playTone(ctx, { frequency: 660, duration: 0.09, type: "sine", volume: 0.18 }); playTone(ctx, { frequency: 880, duration: 0.16, type: "sine", volume: 0.18, delay: 0.09 }); }