From d5f537eb8babdc6d9f47442f4fe403f14ceba80b Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Thu, 30 Apr 2026 10:06:00 +0200 Subject: [PATCH] feat: add game music loop and mallette sounds --- src/components/three/MainFeatureObject.tsx | 11 ++-- src/managers/AudioManager.ts | 64 ++++++++++++++++++++++ src/world/GameMusic.tsx | 18 ++++++ src/world/World.tsx | 6 +- 4 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 src/world/GameMusic.tsx diff --git a/src/components/three/MainFeatureObject.tsx b/src/components/three/MainFeatureObject.tsx index 4832966..6515bc9 100644 --- a/src/components/three/MainFeatureObject.tsx +++ b/src/components/three/MainFeatureObject.tsx @@ -10,9 +10,8 @@ interface MainFeatureObjectProps { } const CASE_MODEL_PATH = "/models/packderelance/model.gltf"; -const CASE_SOUND_PATH = "/sounds/effect/fa.mp3"; -const CASE_OPEN_SOUND_RATE = 1.08; -const CASE_CLOSE_SOUND_RATE = 0.82; +const CASE_OPEN_SOUND_PATH = "/sounds/effect/open-malette.mp3"; +const CASE_CLOSE_SOUND_PATH = "/sounds/effect/close-malette.mp3"; export function MainFeatureObject({ position, @@ -25,9 +24,9 @@ export function MainFeatureObject({ colliders="cuboid" label={open ? "Fermer la mallette" : "Ouvrir la mallette"} onTrigger={() => { - AudioManager.getInstance().playSound(CASE_SOUND_PATH, 1, { - playbackRate: open ? CASE_CLOSE_SOUND_RATE : CASE_OPEN_SOUND_RATE, - }); + AudioManager.getInstance().playSound( + open ? CASE_CLOSE_SOUND_PATH : CASE_OPEN_SOUND_PATH, + ); onToggle(); }} > diff --git a/src/managers/AudioManager.ts b/src/managers/AudioManager.ts index 013c33f..bbd3741 100644 --- a/src/managers/AudioManager.ts +++ b/src/managers/AudioManager.ts @@ -7,6 +7,9 @@ interface PlaySoundOptions { export class AudioManager { private static _instance: AudioManager | null = null; private readonly _audioPools = new Map(); + 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 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 { + this.stopMusic(); this._audioPools.forEach((pool) => { pool.forEach((audio) => { audio.pause(); @@ -80,6 +120,30 @@ export class AudioManager { 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 { if (error instanceof Error || error instanceof DOMException) { return error; diff --git a/src/world/GameMusic.tsx b/src/world/GameMusic.tsx new file mode 100644 index 0000000..a452c5d --- /dev/null +++ b/src/world/GameMusic.tsx @@ -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; +} diff --git a/src/world/World.tsx b/src/world/World.tsx index 98f9e51..abed386 100644 --- a/src/world/World.tsx +++ b/src/world/World.tsx @@ -9,6 +9,7 @@ import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls"; import { DebugHelpers } from "@/components/debug/scene/DebugHelpers"; import { Environment } from "@/world/Environment"; +import { GameMusic } from "@/world/GameMusic"; import { Lighting } from "@/world/Lighting"; import { GameMap } from "@/world/GameMap"; import { Player } from "@/world/player/Player"; @@ -31,7 +32,10 @@ export function World(): React.JSX.Element { {cameraMode === "debug" ? : null} {sceneMode === "game" ? ( - + <> + + + ) : ( )}