feat: add game music loop and mallette sounds

This commit is contained in:
Tom Boullay
2026-04-30 10:06:00 +02:00
parent 01c583ba96
commit 92097e5256
4 changed files with 92 additions and 7 deletions
+5 -6
View File
@@ -10,9 +10,8 @@ interface MainFeatureObjectProps {
} }
const CASE_MODEL_PATH = "/models/packderelance/model.gltf"; const CASE_MODEL_PATH = "/models/packderelance/model.gltf";
const CASE_SOUND_PATH = "/sounds/effect/fa.mp3"; const CASE_OPEN_SOUND_PATH = "/sounds/effect/open-malette.mp3";
const CASE_OPEN_SOUND_RATE = 1.08; const CASE_CLOSE_SOUND_PATH = "/sounds/effect/close-malette.mp3";
const CASE_CLOSE_SOUND_RATE = 0.82;
export function MainFeatureObject({ export function MainFeatureObject({
position, position,
@@ -25,9 +24,9 @@ export function MainFeatureObject({
colliders="cuboid" colliders="cuboid"
label={open ? "Fermer la mallette" : "Ouvrir la mallette"} label={open ? "Fermer la mallette" : "Ouvrir la mallette"}
onTrigger={() => { onTrigger={() => {
AudioManager.getInstance().playSound(CASE_SOUND_PATH, 1, { AudioManager.getInstance().playSound(
playbackRate: open ? CASE_CLOSE_SOUND_RATE : CASE_OPEN_SOUND_RATE, open ? CASE_CLOSE_SOUND_PATH : CASE_OPEN_SOUND_PATH,
}); );
onToggle(); onToggle();
}} }}
> >
+64
View File
@@ -7,6 +7,9 @@ interface PlaySoundOptions {
export class AudioManager { export class AudioManager {
private static _instance: AudioManager | null = null; private static _instance: AudioManager | null = null;
private readonly _audioPools = new Map<string, HTMLAudioElement[]>(); private readonly _audioPools = new Map<string, HTMLAudioElement[]>();
private _music: HTMLAudioElement | null = null;
private _musicPath: string | null = null;
private _musicUnlockHandler: (() => void) | null = null;
private static readonly MAX_POOL_SIZE_PER_SOUND = 6; private static readonly MAX_POOL_SIZE_PER_SOUND = 6;
private static readonly IGNORED_PLAYBACK_ERRORS = new Set([ private static readonly IGNORED_PLAYBACK_ERRORS = new Set([
@@ -45,7 +48,44 @@ export class AudioManager {
}); });
} }
playMusic(path: string, volume = 1): void {
if (this._musicPath === path && this._music) {
this._music.volume = Math.max(0, Math.min(1, volume));
if (!this._music.paused) return;
} else {
this.stopMusic();
this._music = new Audio(path);
this._music.loop = true;
this._musicPath = path;
}
this._music.volume = Math.max(0, Math.min(1, volume));
void this._music.play().catch((error: unknown) => {
if (
error instanceof DOMException &&
AudioManager.IGNORED_PLAYBACK_ERRORS.has(error.name)
) {
this._waitForUserGestureToPlayMusic();
return;
}
logger.error("AudioManager", "Failed to play music", {
path,
error: AudioManager._toLogValue(error),
});
});
}
stopMusic(): void {
this._removeMusicUnlockHandler();
this._music?.pause();
this._music = null;
this._musicPath = null;
}
destroy(): void { destroy(): void {
this.stopMusic();
this._audioPools.forEach((pool) => { this._audioPools.forEach((pool) => {
pool.forEach((audio) => { pool.forEach((audio) => {
audio.pause(); audio.pause();
@@ -80,6 +120,30 @@ export class AudioManager {
return initialAudio; return initialAudio;
} }
private _waitForUserGestureToPlayMusic(): void {
if (this._musicUnlockHandler) return;
this._musicUnlockHandler = () => {
this._removeMusicUnlockHandler();
void this._music?.play();
};
window.addEventListener("pointerdown", this._musicUnlockHandler, {
once: true,
});
window.addEventListener("keydown", this._musicUnlockHandler, {
once: true,
});
}
private _removeMusicUnlockHandler(): void {
if (!this._musicUnlockHandler) return;
window.removeEventListener("pointerdown", this._musicUnlockHandler);
window.removeEventListener("keydown", this._musicUnlockHandler);
this._musicUnlockHandler = null;
}
private static _toLogValue(error: unknown): Error | DOMException | string { private static _toLogValue(error: unknown): Error | DOMException | string {
if (error instanceof Error || error instanceof DOMException) { if (error instanceof Error || error instanceof DOMException) {
return error; return error;
+18
View File
@@ -0,0 +1,18 @@
import { useEffect } from "react";
import { AudioManager } from "@/managers/AudioManager";
const GAME_MUSIC_PATH = "/sounds/musique/test.mp3";
const GAME_MUSIC_VOLUME = 0.45;
export function GameMusic(): null {
useEffect(() => {
const audio = AudioManager.getInstance();
audio.playMusic(GAME_MUSIC_PATH, GAME_MUSIC_VOLUME);
return () => {
audio.stopMusic();
};
}, []);
return null;
}
+5 -1
View File
@@ -9,6 +9,7 @@ import { useSceneMode } from "@/hooks/debug/useSceneMode";
import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls"; import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls";
import { DebugHelpers } from "@/components/debug/scene/DebugHelpers"; import { DebugHelpers } from "@/components/debug/scene/DebugHelpers";
import { Environment } from "@/world/Environment"; import { Environment } from "@/world/Environment";
import { GameMusic } from "@/world/GameMusic";
import { Lighting } from "@/world/Lighting"; import { Lighting } from "@/world/Lighting";
import { GameMap } from "@/world/GameMap"; import { GameMap } from "@/world/GameMap";
import { Player } from "@/world/player/Player"; import { Player } from "@/world/player/Player";
@@ -31,7 +32,10 @@ export function World(): React.JSX.Element {
{cameraMode === "debug" ? <DebugCameraControls /> : null} {cameraMode === "debug" ? <DebugCameraControls /> : null}
{sceneMode === "game" ? ( {sceneMode === "game" ? (
<GameMap onOctreeReady={setOctree} /> <>
<GameMusic />
<GameMap onOctreeReady={setOctree} />
</>
) : ( ) : (
<TestMap onOctreeReady={setOctree} /> <TestMap onOctreeReady={setOctree} />
)} )}