generated from nhcarrigan/template
49bfc6a109
Adds Web Audio API sound effects and browser notifications for key game events: achievement unlocked, quest completed, quest failed, boss defeated, prestige, transcendence, and apotheosis. Both features are toggled via profile settings, with notification permission requested on first enable.
111 lines
3.0 KiB
TypeScript
111 lines
3.0 KiB
TypeScript
/**
|
|
* @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<number>;
|
|
gain: number;
|
|
noteDuration: number;
|
|
type: OscillatorType;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- intentional constant name
|
|
const SOUND_PATTERNS: Record<SoundEvent, SoundPattern> = {
|
|
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 };
|