diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md
index f502ff1..f25359b 100644
--- a/docs/technical/architecture.md
+++ b/docs/technical/architecture.md
@@ -14,7 +14,7 @@ This document describes the code that exists today in the repository.
- either the map scene or the debug physics test scene
- the player rig when the active camera mode is `player`
- `src/world/GameMap.tsx` loads map nodes from `public/map.json`, resolves available models, and builds the collision octree.
-- `src/world/debug/TestScene.tsx` provides a debug-oriented interaction and physics scene.
+- `src/world/debug/TestMap.tsx` provides a debug-oriented interaction and physics map.
- `src/world/player/PlayerComponent.tsx` mounts the camera and controller.
- `src/world/player/PlayerController.tsx` owns pointer lock movement, jump handling, and interaction input.
@@ -64,7 +64,7 @@ This document describes the code that exists today in the repository.
## Current Limitations
- The repository is a prototype, not the full intended game runtime.
-- `src/world/debug/TestScene.tsx` is part of the active scene composition.
+- `src/world/debug/TestMap.tsx` is part of the active scene composition.
- There is no central gameplay orchestrator such as `GameManager`.
- Missions, zones, cinematics, and dialogue systems are not implemented.
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
diff --git a/public/assets/UI/cassé.webm b/public/assets/UI/cassé.webm
new file mode 100644
index 0000000..bbbe317
--- /dev/null
+++ b/public/assets/UI/cassé.webm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:56f019508cbcb5c0c4770bdad0816c7b1d332fc809d582ee4e0d90904415c745
+size 252779
diff --git a/public/assets/UI/centrale.webm b/public/assets/UI/centrale.webm
new file mode 100644
index 0000000..8287a94
--- /dev/null
+++ b/public/assets/UI/centrale.webm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dd2b2be96baaab171a5f68e23c785a0ea3583c7ba71b5a9b5745213fc3b0f5f8
+size 225885
diff --git a/public/assets/UI/ebike.webm b/public/assets/UI/ebike.webm
new file mode 100644
index 0000000..003fbd1
--- /dev/null
+++ b/public/assets/UI/ebike.webm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:07684c556587213b7deae75e153594d8299acde4d3a28bfe88cbd49abdcda880
+size 197560
diff --git a/public/assets/UI/interagir.webm b/public/assets/UI/interagir.webm
new file mode 100644
index 0000000..d38a4c4
--- /dev/null
+++ b/public/assets/UI/interagir.webm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1dd42a107d015aa0028823e248a3a3e32e6b8cb90173b547675145fefdba4581
+size 272167
diff --git a/public/assets/UI/laferme.webm b/public/assets/UI/laferme.webm
new file mode 100644
index 0000000..4b16f79
--- /dev/null
+++ b/public/assets/UI/laferme.webm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c5872877c56253cb364aceb0f39551143e142150807c7e837759677b16d22741
+size 206993
diff --git a/public/assets/logo/logo.jpg b/public/assets/logo/logo.jpg
new file mode 100644
index 0000000..3617df6
--- /dev/null
+++ b/public/assets/logo/logo.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:814db18091a1a822dc2ebdef9f00400c4ff943e9aa1e43151e85b6ea1c4e98cc
+size 149572
diff --git a/public/models/sky/model.glb b/public/models/sky/model.glb
new file mode 100644
index 0000000..0f3feb7
--- /dev/null
+++ b/public/models/sky/model.glb
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8f2a39ec378538c2b28064437f794963fad4f3091dc960e5feca63dff7c60d85
+size 329420
diff --git a/public/skybox/sky.exr b/public/skybox/sky.exr
deleted file mode 100644
index c6ff8e1..0000000
--- a/public/skybox/sky.exr
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:904b303c98f865526b9524b955f440e630f29d8e18a57bb7bf443fcd9715add1
-size 83079911
diff --git a/public/sounds/effect/close-malette.mp3 b/public/sounds/effect/close-malette.mp3
new file mode 100644
index 0000000..1f3f1e8
--- /dev/null
+++ b/public/sounds/effect/close-malette.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4bb923d3b44ca6ebecdfcc45a6cba4a5666d7f7ecb66250fc427d7dcb6b5b3b1
+size 25077
diff --git a/public/sounds/fa.mp3 b/public/sounds/effect/fa.mp3
similarity index 100%
rename from public/sounds/fa.mp3
rename to public/sounds/effect/fa.mp3
diff --git a/public/sounds/effect/open-malette.mp3 b/public/sounds/effect/open-malette.mp3
new file mode 100644
index 0000000..44433f6
--- /dev/null
+++ b/public/sounds/effect/open-malette.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:93c7a033aa7eef8ce9fa012b5ad3ef1fa961e1f842e21641fc02025ee4d6f4fd
+size 12288
diff --git a/public/sounds/musique/test.mp3 b/public/sounds/musique/test.mp3
new file mode 100644
index 0000000..d4095f6
--- /dev/null
+++ b/public/sounds/musique/test.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de4036838a022ff91e10dabf4db2c0e9e41f49c5fc6e59b571100d6aaba29cd2
+size 48225313
diff --git a/src/components/three/MainFeatureObject.tsx b/src/components/three/MainFeatureObject.tsx
new file mode 100644
index 0000000..4832966
--- /dev/null
+++ b/src/components/three/MainFeatureObject.tsx
@@ -0,0 +1,42 @@
+import { TriggerObject } from "@/components/three/TriggerObject";
+import { RepairCaseModel } from "@/components/three/RepairCaseModel";
+import { AudioManager } from "@/managers/AudioManager";
+import type { Vector3Tuple } from "@/types/three";
+
+interface MainFeatureObjectProps {
+ position: Vector3Tuple;
+ open: boolean;
+ onToggle: () => void;
+}
+
+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;
+
+export function MainFeatureObject({
+ position,
+ open,
+ onToggle,
+}: MainFeatureObjectProps): React.JSX.Element {
+ return (
+ {
+ AudioManager.getInstance().playSound(CASE_SOUND_PATH, 1, {
+ playbackRate: open ? CASE_CLOSE_SOUND_RATE : CASE_OPEN_SOUND_RATE,
+ });
+ onToggle();
+ }}
+ >
+
+
+ );
+}
diff --git a/src/components/three/MainFeatureZone.tsx b/src/components/three/MainFeatureZone.tsx
index 31fd500..a0a3388 100644
--- a/src/components/three/MainFeatureZone.tsx
+++ b/src/components/three/MainFeatureZone.tsx
@@ -1,11 +1,9 @@
import { useState } from "react";
import { Text } from "@react-three/drei";
-import { TriggerObject } from "@/components/three/TriggerObject";
+import { MainFeatureObject } from "@/components/three/MainFeatureObject";
import { ModelSelectorPlaceholder } from "@/components/three/ModelSelectorPlaceholder";
-import { RepairCaseModel } from "@/components/three/RepairCaseModel";
const ZONE_ORIGIN = [10, 0.4, -8] as const;
-const CASE_MODEL_PATH = "/models/packderelance/model.gltf";
const ZONE_RADIUS = 4.2;
export function MainFeatureZone(): React.JSX.Element {
@@ -44,19 +42,11 @@ export function MainFeatureZone(): React.JSX.Element {
Pack de Relance Feature
- setCaseOpen((value) => !value)}
- >
-
-
+ open={caseOpen}
+ onToggle={() => setCaseOpen((value) => !value)}
+ />
scene.clone(true), [scene]);
const lidRef = useRef(null);
- const closedRotationX = useRef(0);
+ const initialOpen = useRef(open);
+ const openedRotationZ = useRef(0);
const parsedScale =
typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale;
useEffect(() => {
const lid = model.getObjectByName(CASE_LID_NODE_NAME);
lidRef.current = lid ?? null;
- closedRotationX.current = lid?.rotation.x ?? 0;
+ openedRotationZ.current = lid?.rotation.z ?? 0;
+
+ if (lid && !initialOpen.current) {
+ lid.rotation.z = openedRotationZ.current + CASE_CLOSED_ROTATION_OFFSET_Z;
+ }
}, [model]);
- useFrame((_, delta) => {
+ useEffect(() => {
const lid = lidRef.current;
if (!lid) return;
const targetRotation =
- closedRotationX.current - (open ? CASE_OPEN_ANGLE : 0);
- lid.rotation.x = THREE.MathUtils.damp(
- lid.rotation.x,
- targetRotation,
- CASE_OPEN_SPEED,
- delta,
- );
- });
+ openedRotationZ.current +
+ (open ? CASE_OPEN_ROTATION_OFFSET_Z : CASE_CLOSED_ROTATION_OFFSET_Z);
+ gsap.to(lid.rotation, {
+ z: targetRotation,
+ duration: CASE_ANIMATION_DURATION,
+ ease: "power2.inOut",
+ overwrite: true,
+ });
+
+ return () => {
+ gsap.killTweensOf(lid.rotation);
+ };
+ }, [open]);
return (
diff --git a/src/components/three/SkyModel.tsx b/src/components/three/SkyModel.tsx
new file mode 100644
index 0000000..c0b1a8e
--- /dev/null
+++ b/src/components/three/SkyModel.tsx
@@ -0,0 +1,29 @@
+import { useFrame, useThree } from "@react-three/fiber";
+import { useGLTF } from "@react-three/drei";
+import { useMemo, useRef } from "react";
+import * as THREE from "three";
+
+interface SkyModelProps {
+ modelPath: string;
+}
+
+const SKY_MODEL_SCALE = 1;
+
+export function SkyModel({ modelPath }: SkyModelProps): React.JSX.Element {
+ const camera = useThree((state) => state.camera);
+ const groupRef = useRef(null);
+ const { scene } = useGLTF(modelPath);
+ const model = useMemo(() => scene.clone(true), [scene]);
+
+ useFrame(() => {
+ groupRef.current?.position.copy(camera.position);
+ });
+
+ return (
+
+
+
+ );
+}
+
+useGLTF.preload("/models/sky/model.glb");
diff --git a/src/components/three/index.ts b/src/components/three/index.ts
index 31fe607..16040d2 100644
--- a/src/components/three/index.ts
+++ b/src/components/three/index.ts
@@ -6,6 +6,7 @@ export type { SimpleModelConfig } from "./SimpleModel";
export { ExplodableModel } from "./ExplodableModel";
export { MainFeatureZone } from "./MainFeatureZone";
+export { MainFeatureObject } from "./MainFeatureObject";
export { ModelSelectorPlaceholder } from "./ModelSelectorPlaceholder";
export { RepairCaseModel } from "./RepairCaseModel";
diff --git a/src/components/ui/HandTrackingProvider.tsx b/src/components/ui/HandTrackingProvider.tsx
index 36e9a36..f404c24 100644
--- a/src/components/ui/HandTrackingProvider.tsx
+++ b/src/components/ui/HandTrackingProvider.tsx
@@ -6,7 +6,6 @@ import {
HandTrackingContext,
} from "@/hooks/useHandTrackingSnapshot";
import { useRemoteHandTracking } from "@/hooks/useRemoteHandTracking";
-import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
export function HandTrackingProvider({
children,
@@ -15,10 +14,7 @@ export function HandTrackingProvider({
}): React.JSX.Element {
const sceneMode = useSceneMode();
const { nearby, holding, handHolding } = useInteraction();
- const enabled =
- isDebugEnabled() &&
- sceneMode === "physics" &&
- (nearby || holding || handHolding);
+ const enabled = sceneMode === "physics" && (nearby || holding || handHolding);
const snapshot = useRemoteHandTracking({ enabled });
return (
diff --git a/src/data/debug/testSceneConfig.ts b/src/data/debug/testSceneConfig.ts
index da1edc2..545db0b 100644
--- a/src/data/debug/testSceneConfig.ts
+++ b/src/data/debug/testSceneConfig.ts
@@ -13,7 +13,7 @@ export const TEST_SCENE_GRABBABLE_ROUGHNESS = 0.6;
export const TEST_SCENE_GRABBABLE_METALNESS = 0.1;
export const TEST_SCENE_TRIGGER_POSITION: Vector3Tuple = [3, 2, -3];
-export const TEST_SCENE_TRIGGER_SOUND_PATH = "/sounds/fa.mp3";
+export const TEST_SCENE_TRIGGER_SOUND_PATH = "/sounds/effect/fa.mp3";
export const TEST_SCENE_TRIGGER_RADIUS = 0.4;
export const TEST_SCENE_TRIGGER_SEGMENTS = 32;
export const TEST_SCENE_TRIGGER_COLOR = "#3b82f6";
diff --git a/src/data/docs/docsTranslations.ts b/src/data/docs/docsTranslations.ts
index 91a3f4f..24f16a0 100644
--- a/src/data/docs/docsTranslations.ts
+++ b/src/data/docs/docsTranslations.ts
@@ -99,7 +99,7 @@ Ce document décrit le code réellement présent aujourd'hui dans le dépôt.
- soit la carte principale, soit la scène de test physique debug
- le rig joueur quand le mode caméra actif est \`player\`
- \`src/world/GameMap.tsx\` charge les modèles de carte disponibles et construit l'octree de collision.
-- \`src/world/debug/TestScene.tsx\` fournit une scène orientée debug pour les interactions et la physique.
+- \`src/world/debug/TestMap.tsx\` fournit une carte orientée debug pour les interactions et la physique.
- \`src/world/player/Player.tsx\` monte la caméra et le contrôleur.
- \`src/world/player/PlayerController.tsx\` gère le mouvement pointer lock, le saut et les inputs d'interaction.
@@ -129,7 +129,7 @@ Ce document décrit le code réellement présent aujourd'hui dans le dépôt.
## Limites actuelles
- Le dépôt est encore un prototype, pas le runtime complet du jeu.
-- \`src/world/debug/TestScene.tsx\` fait encore partie de la composition active.
+- \`src/world/debug/TestMap.tsx\` fait encore partie de la composition active.
- Il n'existe pas encore d'orchestrateur gameplay central comme \`GameManager\`.
- Les systèmes de missions, zones, cinématiques et dialogues ne sont pas implémentés.
- Le joueur utilise une collision octree et des règles simples, pas une pile physique gameplay complète.
diff --git a/src/data/world/environmentConfig.ts b/src/data/world/environmentConfig.ts
index fe277fa..f5f1222 100644
--- a/src/data/world/environmentConfig.ts
+++ b/src/data/world/environmentConfig.ts
@@ -1,2 +1,2 @@
-export const GAME_SCENE_SKYBOX_PATH = "/skybox/sky.exr";
+export const GAME_SCENE_SKY_MODEL_PATH = "/models/sky/model.glb";
export const PHYSICS_SCENE_BACKGROUND_COLOR = "#0b1018";
diff --git a/src/managers/AudioManager.ts b/src/managers/AudioManager.ts
index 1fcc256..013c33f 100644
--- a/src/managers/AudioManager.ts
+++ b/src/managers/AudioManager.ts
@@ -1,5 +1,9 @@
import { logger } from "@/utils/logger";
+interface PlaySoundOptions {
+ playbackRate?: number;
+}
+
export class AudioManager {
private static _instance: AudioManager | null = null;
private readonly _audioPools = new Map();
@@ -20,9 +24,10 @@ export class AudioManager {
private constructor() {}
- playSound(path: string, volume = 1): void {
+ playSound(path: string, volume = 1, options: PlaySoundOptions = {}): void {
const audio = this._acquireAudio(path);
audio.volume = Math.max(0, Math.min(1, volume));
+ audio.playbackRate = options.playbackRate ?? 1;
audio.currentTime = 0;
void audio.play().catch((error: unknown) => {
diff --git a/src/world/Environment.tsx b/src/world/Environment.tsx
index b81a4ef..bd71cfe 100644
--- a/src/world/Environment.tsx
+++ b/src/world/Environment.tsx
@@ -1,9 +1,9 @@
-import { Environment as DreiEnvironment } from "@react-three/drei";
import {
- GAME_SCENE_SKYBOX_PATH,
+ GAME_SCENE_SKY_MODEL_PATH,
PHYSICS_SCENE_BACKGROUND_COLOR,
} from "@/data/world/environmentConfig";
import { useSceneMode } from "@/hooks/debug/useSceneMode";
+import { SkyModel } from "@/components/three/SkyModel";
export function Environment(): React.JSX.Element {
const sceneMode = useSceneMode();
@@ -14,5 +14,5 @@ export function Environment(): React.JSX.Element {
);
}
- return ;
+ return ;
}
diff --git a/src/world/World.tsx b/src/world/World.tsx
index b945c38..98f9e51 100644
--- a/src/world/World.tsx
+++ b/src/world/World.tsx
@@ -12,7 +12,7 @@ import { Environment } from "@/world/Environment";
import { Lighting } from "@/world/Lighting";
import { GameMap } from "@/world/GameMap";
import { Player } from "@/world/player/Player";
-import { TestScene } from "@/world/debug/TestScene";
+import { TestMap } from "@/world/debug/TestMap";
export function World(): React.JSX.Element {
const cameraMode = useCameraMode();
@@ -33,7 +33,7 @@ export function World(): React.JSX.Element {
{sceneMode === "game" ? (
) : (
-
+
)}
{cameraMode !== "debug" ? (
diff --git a/src/world/debug/TestScene.tsx b/src/world/debug/TestMap.tsx
similarity index 96%
rename from src/world/debug/TestScene.tsx
rename to src/world/debug/TestMap.tsx
index 6b71af1..ee1a3ff 100644
--- a/src/world/debug/TestScene.tsx
+++ b/src/world/debug/TestMap.tsx
@@ -24,13 +24,11 @@ import {
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
import type { OctreeReadyHandler } from "@/types/three";
-interface TestSceneProps {
+interface TestMapProps {
onOctreeReady: OctreeReadyHandler;
}
-export function TestScene({
- onOctreeReady,
-}: TestSceneProps): React.JSX.Element {
+export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
const floorRef = useRef(null);
useOctreeGraphNode(floorRef, onOctreeReady);