Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { useEffect, useRef } from 'react';
import { Howl } from 'howler';
import useGameStore from '../stores/useGameStore';
import usePreferences from '../stores/usePreferences';
import useAudio from '../hooks/useAudio';
import placeSoundUrl from '../assets/audio/go-stone-placing.mp3';
import captureSoundUrl from '../assets/audio/go-stone-removal.mp3';

const placeSound = new Howl({ src: [placeSoundUrl], volume: 0.5 });
const captureSound = new Howl({ src: [captureSoundUrl], volume: 0.5 });

const THROTTLE_MS = 150;

export default function SoundEffects() {
const game = useGameStore((state) => state.game);
const soundEnabled = usePreferences((state) => state.soundEnabled);
const sounds = useAudio({ place: placeSoundUrl, capture: captureSoundUrl });
const prevRef = useRef({ move: 0, captures: 0 });
const lastPlayedRef = useRef(0);

Expand All @@ -29,14 +27,13 @@ export default function SoundEffects() {

if (!soundEnabled || (!placed && !captured)) return;

// Prevent audio spam when scrubbing quickly (e.g. holding arrow keys)
const now = performance.now();
if (now - lastPlayedRef.current < THROTTLE_MS) return;
lastPlayedRef.current = now;

if (placed) placeSound.play();
if (captured) captureSound.play();
}, [game, soundEnabled]);
if (placed) sounds.place.play();
if (captured) sounds.capture.play();
}, [game, soundEnabled, sounds]);

return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useEffect, useRef } from 'react';
import { Howl, Howler } from 'howler';

type SoundConfig = { src: string; volume?: number };
type SoundMap<T extends string> = Record<T, SoundConfig | string>;
type Sounds<T extends string> = Record<T, Howl>;

function createSounds<T extends string>(map: SoundMap<T>): Sounds<T> {
const entries = Object.entries<SoundConfig | string>(map).map(([key, value]) => {
const config = typeof value === 'string' ? { src: value } : value;
return [key, new Howl({ src: [config.src], volume: config.volume ?? 0.5 })];
});
return Object.fromEntries(entries) as Sounds<T>;
}

export default function useAudio<T extends string>(map: SoundMap<T>): Sounds<T> {
const soundsRef = useRef<Sounds<T>>(createSounds(map));

useEffect(() => {
function cleanup() {
window.removeEventListener('pointerdown', resume);
window.removeEventListener('keydown', resume);
}

function resume() {
Howler.unload();
soundsRef.current = createSounds(map);
cleanup();
}

const handleVisibilityChange = () => {
if (document.visibilityState !== 'visible') return;
const state = Howler.ctx?.state as string;
if (state !== 'interrupted' && state !== 'suspended') return;

window.addEventListener('pointerdown', resume);
window.addEventListener('keydown', resume);
};

document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
cleanup();
};
}, []);

return soundsRef.current;
}
Loading