/** * @file Sound effect utilities using the Web Audio API. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ type SoundEvent = | "achievement" | "apotheosis" | "bossVictory" | "prestige" | "questCompleted" | "questFailed" | "transcendence"; interface SoundPattern { frequencies: Array; gain: number; noteDuration: number; type: OscillatorType; } // eslint-disable-next-line @typescript-eslint/naming-convention -- intentional constant name const SOUND_PATTERNS: Record = { achievement: { frequencies: [ 523, 659, 784, 1047 ], gain: 0.3, noteDuration: 0.12, type: "triangle", }, apotheosis: { frequencies: [ 1047, 880, 784, 659, 523 ], gain: 0.35, noteDuration: 0.25, type: "sine", }, bossVictory: { frequencies: [ 523, 784, 1047 ], gain: 0.4, noteDuration: 0.18, type: "square", }, prestige: { frequencies: [ 392, 523, 659, 784 ], gain: 0.35, noteDuration: 0.15, type: "sawtooth", }, questCompleted: { frequencies: [ 523, 659 ], gain: 0.25, noteDuration: 0.15, type: "sine", }, questFailed: { frequencies: [ 392, 330, 261 ], gain: 0.25, noteDuration: 0.18, type: "triangle", }, transcendence: { frequencies: [ 261, 329, 392, 523 ], gain: 0.3, noteDuration: 0.3, type: "sine", }, }; // eslint-disable-next-line @typescript-eslint/init-declarations -- lazily initialised on first use let audioContext: AudioContext | undefined; const getAudioContext = (): AudioContext => { if (audioContext === undefined) { audioContext = new AudioContext(); } return audioContext; }; /** * Plays a sound effect for a given game event using the Web Audio API. * @param event - The game event to play a sound for. */ const playSound = (event: SoundEvent): void => { try { const context = getAudioContext(); const pattern = SOUND_PATTERNS[event]; for (const [ index, frequency ] of pattern.frequencies.entries()) { const oscillator = context.createOscillator(); const gainNode = context.createGain(); oscillator.connect(gainNode); gainNode.connect(context.destination); oscillator.type = pattern.type; oscillator.frequency.value = frequency; const noteOffset = index * pattern.noteDuration; const startTime = context.currentTime + noteOffset; const endTime = startTime + pattern.noteDuration; gainNode.gain.setValueAtTime(0, startTime); gainNode.gain.linearRampToValueAtTime(pattern.gain, startTime + 0.01); gainNode.gain.linearRampToValueAtTime(0, endTime); oscillator.start(startTime); oscillator.stop(endTime); } } catch { // Silently ignore — audio may not be available in all environments } }; export type { SoundEvent }; export { playSound };