feat: add game music loop and mallette sounds
This commit is contained in:
@@ -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();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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" ? (
|
||||||
|
<>
|
||||||
|
<GameMusic />
|
||||||
<GameMap onOctreeReady={setOctree} />
|
<GameMap onOctreeReady={setOctree} />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<TestMap onOctreeReady={setOctree} />
|
<TestMap onOctreeReady={setOctree} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user