Add working sound effects and background music via Web Audio API
CI / build-and-push (push) Successful in 1m17s

Synthesized audio system with 9 distinct SFX (click, success, warning,
danger, purchase, achievement, era transition, info) mapped to all game
notifications, plus generative ambient background music with chord
progressions. Adds SFX volume slider to settings alongside existing
music volume control. No audio files or npm dependencies needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 20:11:50 -04:00
parent 1e3d50719e
commit d609934b73
9 changed files with 701 additions and 7 deletions
+69
View File
@@ -0,0 +1,69 @@
import { AudioManager } from './AudioManager';
import type { GameSettings } from '@token-empire/shared';
import { useGameStore } from '@/store';
export { AudioManager } from './AudioManager';
export { triggerNotificationSound, playUISound } from './sounds';
export type { SoundId } from './synthesizer';
function applyVolumes(audio: AudioManager, settings: GameSettings): void {
audio.setSoundEnabled(settings.soundEnabled);
audio.setMusicVolume(settings.musicVolume);
audio.setSfxVolume(settings.sfxVolume ?? 0.5);
}
function shouldPlayMusic(settings: GameSettings): boolean {
return settings.soundEnabled && settings.musicVolume > 0;
}
export function initAudioSystem(): () => void {
const audio = AudioManager.getInstance();
let gestureListenerActive = false;
const settings = useGameStore.getState().meta.settings;
applyVolumes(audio, settings);
// Music requires a user gesture to start (browser autoplay policy).
// We register a one-shot listener that starts music on first click/key.
const startOnGesture = () => {
const s = useGameStore.getState().meta.settings;
if (shouldPlayMusic(s)) {
audio.startMusic();
}
document.removeEventListener('click', startOnGesture);
document.removeEventListener('keydown', startOnGesture);
gestureListenerActive = false;
};
if (shouldPlayMusic(settings)) {
document.addEventListener('click', startOnGesture);
document.addEventListener('keydown', startOnGesture);
gestureListenerActive = true;
}
const unsub = useGameStore.subscribe((state) => {
const next = state.meta.settings;
applyVolumes(audio, next);
if (shouldPlayMusic(next)) {
if (!audio.isMusicPlaying) {
if (audio.isContextActive) {
audio.startMusic();
} else if (!gestureListenerActive) {
document.addEventListener('click', startOnGesture);
document.addEventListener('keydown', startOnGesture);
gestureListenerActive = true;
}
}
} else {
audio.stopMusic();
}
});
return () => {
unsub();
document.removeEventListener('click', startOnGesture);
document.removeEventListener('keydown', startOnGesture);
audio.dispose();
};
}