merge mission & intro
This commit is contained in:
@@ -9,7 +9,9 @@ This document describes the 3D components that are currently used in the runtime
|
||||
| Interaction | `InteractableObject` | Focus detection through distance and raycasting |
|
||||
| Interaction | `TriggerObject` | Press-to-trigger interactions, optional sound, optional spawned model |
|
||||
| Interaction | `GrabbableObject` | Physics grab and hand-tracking grab behavior |
|
||||
| Model | `AnimatedModel` | GLTF animation playback with fade, speed, and context controls |
|
||||
| Model | `ExplodableModel` | Split/reassemble a GLTF model into separated parts |
|
||||
| Model | `SimpleModel` | Lightweight static GLTF render helper |
|
||||
| Gameplay | `RepairCaseModel` | Repair case lid animation, proximity float, and wobble |
|
||||
|
||||
## Continuous Animation
|
||||
@@ -27,6 +29,30 @@ Use GSAP only for discrete timeline-style transitions. Current example:
|
||||
|
||||
- `RepairCaseModel` animates the case lid between open and closed rotations.
|
||||
|
||||
## Animated Models
|
||||
|
||||
`src/components/three/models/AnimatedModel.tsx` wraps drei `useAnimations()` around a loaded GLTF scene.
|
||||
|
||||
It supports:
|
||||
|
||||
- default animation playback
|
||||
- optional autoplay
|
||||
- fade duration
|
||||
- speed multiplier
|
||||
- `onLoaded`
|
||||
- `onAnimationEnd`
|
||||
- context controls through `AnimatedModelContext`
|
||||
|
||||
The debug physics scene currently uses it to preview:
|
||||
|
||||
```txt
|
||||
public/models/electricienne_animated/model.gltf
|
||||
```
|
||||
|
||||
with the `Dance` animation.
|
||||
|
||||
`src/hooks/animation/useCharacterAnimation.ts` is a hook-level alternative for components that need to own their group ref and animation controls directly.
|
||||
|
||||
## GLTF Reuse
|
||||
|
||||
Use `useClonedObject` when a GLTF scene is reused by a component instance. It memoizes `scene.clone(true)` and keeps clone creation out of render churn.
|
||||
@@ -44,7 +70,9 @@ src/components/three/
|
||||
│ ├── InteractableObject.tsx
|
||||
│ └── TriggerObject.tsx
|
||||
├── models/
|
||||
│ └── ExplodableModel.tsx
|
||||
│ ├── AnimatedModel.tsx
|
||||
│ ├── ExplodableModel.tsx
|
||||
│ └── SimpleModel.tsx
|
||||
└── world/
|
||||
└── SkyModel.tsx
|
||||
```
|
||||
|
||||
+268
-106
@@ -4,139 +4,301 @@ This document describes the code that exists today in the repository.
|
||||
|
||||
## Runtime Structure
|
||||
|
||||
- `src/main.tsx` mounts React.
|
||||
- `src/App.tsx` mounts the TanStack `RouterProvider`.
|
||||
- `src/router.tsx` declares the top-level routes:
|
||||
- `/` mounts the playable 3D scene, debug perf overlay, and HTML overlays.
|
||||
- `/editor` mounts the map editor page.
|
||||
- `src/world/World.tsx` composes the active scene, including:
|
||||
- environment and lighting
|
||||
- debug helpers and debug camera mode
|
||||
- either the map scene or the debug physics test scene
|
||||
- the player rig when the active camera mode is `player`
|
||||
- `src/hooks/world/useWorldSceneLoading.ts` owns the production scene loading state shared by `World`, `GameMap`, and the player octree readiness.
|
||||
- `src/world/GameMap.tsx` loads map nodes from `public/map.json`, resolves available models, renders them progressively, and shows fallback cubes for missing models.
|
||||
- `src/world/GameMapCollision.tsx` builds the player collision octree from dedicated collision nodes only.
|
||||
- `src/world/GameStageContent.tsx` is wrapped in Rapier `Physics` in the production game scene so stage gameplay objects can use physics without moving the map or player to Rapier. It now mounts reusable `RepairGame` instances for `bike`, `pylone`, and `ferme` mission states.
|
||||
- `src/world/debug/TestMap.tsx` provides a debug-oriented interaction and physics map with the existing grab/trigger/model-preview objects plus separate `Bike`, `Pylone`, and `Farm` repair playground zones.
|
||||
- `src/world/player/Player.tsx` mounts the camera and controller.
|
||||
- `src/world/player/PlayerController.tsx` owns pointer lock movement, jump handling, repair-step movement locking, and interaction input.
|
||||
- `src/main.tsx` mounts React in `StrictMode`.
|
||||
- `src/App.tsx` mounts TanStack `RouterProvider`.
|
||||
- `src/router.tsx` declares `/`, `/editor`, and `/docs`.
|
||||
- `src/pages/page.tsx` composes the playable route with `HandTrackingProvider`, React Three Fiber `Canvas`, `World`, `DebugPerf`, `GameUI`, and `SceneLoadingOverlay`.
|
||||
- `src/pages/editor/page.tsx` composes the local editor route.
|
||||
- `src/components/docs/DocsLayout.tsx` composes the in-app documentation route.
|
||||
|
||||
Detailed runtime-loading notes live in `docs/technical/scene-runtime.md`.
|
||||
|
||||
## World Composition
|
||||
|
||||
`src/world/World.tsx` is the main 3D scene composer.
|
||||
|
||||
Always-mounted scene systems:
|
||||
|
||||
- `Environment`
|
||||
- `Lighting`
|
||||
- debug helpers when `?debug` is active
|
||||
- optional hand-tracking glove overlays
|
||||
- optional debug camera controls
|
||||
|
||||
Game scene systems:
|
||||
|
||||
- `GameMap`
|
||||
- Rapier `Physics` wrapping `GameStageContent`
|
||||
- `GameMusic`
|
||||
- `GameDialogues`
|
||||
- `GameCinematics` only while `mainState === "outro"`
|
||||
- `Player` after gameplay is ready
|
||||
|
||||
Debug physics scene systems:
|
||||
|
||||
- `TestMap`
|
||||
- `Player` after the debug octree is ready
|
||||
|
||||
Debug scene and camera mode are controlled by `src/utils/debug/Debug.ts` and enabled with `?debug`.
|
||||
|
||||
## Scene Loading
|
||||
|
||||
The production game scene is considered ready only after:
|
||||
|
||||
- map data and visible map nodes have settled
|
||||
- collision source models have settled
|
||||
- the player octree exists
|
||||
- the Rapier gameplay stage has mounted
|
||||
|
||||
The player is not spawned until that readiness gate is satisfied. This avoids starting player movement, music, dialogue timing, and interactions while the map/stage is still loading.
|
||||
|
||||
## Physics Boundaries
|
||||
|
||||
The project currently uses two collision layers with separate responsibilities:
|
||||
The project currently uses two collision systems with separate responsibilities:
|
||||
|
||||
- `GameMapCollision` builds an octree used by the player controller for map collision.
|
||||
- The player octree must be built from a small collision-only subset of map nodes. It currently uses the `terrain` node only instead of traversing the full visible map, because building an octree from all rendered props can overload the browser renderer.
|
||||
- `GameStageContent` is wrapped in Rapier `Physics` for gameplay objects such as repair triggers, cases, grabbables, and future mission-specific objects.
|
||||
- `TestMap` owns its own Rapier `Physics` playground so repair gameplay can be tuned per mission state without depending on the production map layout.
|
||||
- Player movement uses a Three.js `Capsule` and an `Octree`.
|
||||
- Gameplay objects use Rapier rigid bodies and colliders.
|
||||
|
||||
Keep the player and map octree outside the Rapier provider until there is a deliberate migration plan. This avoids mixing player movement rules with object physics before the gameplay systems need it.
|
||||
`GameMapCollision` builds the player octree from explicit collision nodes. It currently uses only the `terrain` node.
|
||||
|
||||
`GameStageContent` is wrapped in Rapier `Physics` so repair cases, triggers, and grabbable parts can use physics without migrating the player controller to Rapier.
|
||||
|
||||
This split is deliberate. It keeps the player controller simple while still enabling physical manipulation for gameplay objects.
|
||||
|
||||
## Gameplay Layer
|
||||
|
||||
The current core gameplay feature is the reusable repair game.
|
||||
|
||||
Production placements live in:
|
||||
|
||||
```txt
|
||||
src/world/GameStageContent.tsx
|
||||
```
|
||||
|
||||
The reusable flow lives in:
|
||||
|
||||
```txt
|
||||
src/components/three/gameplay/RepairGame.tsx
|
||||
```
|
||||
|
||||
Mission-specific data lives in:
|
||||
|
||||
```txt
|
||||
src/data/gameplay/repairMissions.ts
|
||||
```
|
||||
|
||||
The repair game supports:
|
||||
|
||||
```txt
|
||||
locked -> waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done
|
||||
```
|
||||
|
||||
Detailed repair-game implementation notes live in `docs/technical/repair-game.md`.
|
||||
|
||||
## State Management
|
||||
|
||||
Durable progression state lives in:
|
||||
|
||||
```txt
|
||||
src/managers/stores/useGameStore.ts
|
||||
```
|
||||
|
||||
It owns:
|
||||
|
||||
- `mainState`
|
||||
- intro state
|
||||
- `bike`, `pylone`, and `ferme` mission state
|
||||
- outro state
|
||||
- `isCinematicPlaying`
|
||||
- progression actions
|
||||
- generic mission actions
|
||||
|
||||
Settings state lives in:
|
||||
|
||||
```txt
|
||||
src/managers/stores/useSettingsStore.ts
|
||||
```
|
||||
|
||||
Subtitle display state lives in:
|
||||
|
||||
```txt
|
||||
src/managers/stores/useSubtitleStore.ts
|
||||
```
|
||||
|
||||
Detailed Zustand notes live in `docs/technical/zustand.md`.
|
||||
|
||||
## Managers
|
||||
|
||||
Managers are used for imperative runtime systems that own browser or frame-adjacent objects.
|
||||
|
||||
Current managers:
|
||||
|
||||
- `src/managers/AudioManager.ts`
|
||||
- `src/managers/InteractionManager.ts`
|
||||
|
||||
`AudioManager` owns `HTMLAudioElement` instances, music playback, one-shot pools, category volumes, and optional stereo panning.
|
||||
|
||||
`InteractionManager` owns focused/nearby/holding state for trigger and grab interactions and exposes a snapshot through `useSyncExternalStore`.
|
||||
|
||||
## Interaction Model
|
||||
|
||||
- `src/managers/InteractionManager.ts` is the current interaction state source.
|
||||
- `src/components/three/interaction/InteractableObject.tsx` handles focus detection through distance and raycasting.
|
||||
- `src/components/three/interaction/TriggerObject.tsx` implements trigger-style interactions.
|
||||
- `src/components/three/interaction/GrabbableObject.tsx` implements hold-and-release interactions.
|
||||
- `src/hooks/interaction/useInteraction.ts` exposes the interaction snapshot to React UI.
|
||||
- `src/components/ui/InteractPrompt.tsx` shows the `E` prompt for trigger interactions.
|
||||
Core interaction files:
|
||||
|
||||
## Audio
|
||||
- `src/components/three/interaction/InteractableObject.tsx`
|
||||
- `src/components/three/interaction/TriggerObject.tsx`
|
||||
- `src/components/three/interaction/GrabbableObject.tsx`
|
||||
- `src/hooks/interaction/useInteraction.ts`
|
||||
- `src/components/ui/InteractPrompt.tsx`
|
||||
|
||||
- `src/managers/AudioManager.ts` provides pooled one-shot playback, looped music playback, category volumes, and optional stereo pan for one-shot sounds.
|
||||
- Supported audio categories are `music`, `sfx`, and `dialogue`.
|
||||
- Trigger interactions may play SFX directly through `AudioManager`.
|
||||
The player controller bridges raw input to semantic interaction actions:
|
||||
|
||||
## Settings Menu
|
||||
- `E` triggers focused trigger objects
|
||||
- primary mouse button grabs focused grabbable objects
|
||||
- hand tracking can grab hand-controlled grabbable objects
|
||||
|
||||
- `src/managers/stores/useSettingsStore.ts` stores settings for music volume, SFX volume, dialogue volume, subtitle visibility, subtitle language, repair runtime, and menu visibility.
|
||||
- `src/components/ui/GameSettingsMenu.tsx` renders the in-game options menu.
|
||||
- `src/components/ui/GameUI.tsx` mounts the settings menu as an HTML overlay outside the canvas.
|
||||
- `Esc` opens and closes the menu, and `src/world/player/PlayerController.tsx` ignores player input while the menu is open.
|
||||
- Volume changes are forwarded to `AudioManager` by category.
|
||||
Detailed interaction notes live in `docs/technical/interaction.md`.
|
||||
|
||||
## Dialogues And Subtitles
|
||||
## Audio, Dialogue, And Subtitles
|
||||
|
||||
- `public/sounds/dialogue/dialogues.json` is the runtime dialogue manifest.
|
||||
- Dialogue audio files live under `public/sounds/dialogue/`.
|
||||
- Subtitle files live under `public/sounds/dialogue/subtitles/{fr|en}/`.
|
||||
- The current subtitle model is one SRT file per voice and language.
|
||||
- `src/types/dialogues/dialogues.ts` contains the dialogue manifest types.
|
||||
- `src/utils/dialogues/dialogueManifestValidation.ts` validates manifest shape at runtime.
|
||||
- `src/utils/dialogues/loadDialogueManifest.ts` loads the manifest and SRT cues, with French fallback when the selected language is missing.
|
||||
- `src/utils/subtitles/parseSrt.ts` parses SRT blocks and timecodes.
|
||||
- `src/utils/dialogues/playDialogue.ts` plays dialogue audio and synchronizes the active subtitle against the audio element time.
|
||||
- `src/managers/stores/useSubtitleStore.ts` stores the currently displayed subtitle cue.
|
||||
- `src/components/ui/Subtitles.tsx` renders the subtitle overlay.
|
||||
- `src/world/GameDialogues.tsx` currently triggers dialogue entries that define a `timecode`.
|
||||
- Dialogue playback is queued so multiple dialogue requests do not overlap.
|
||||
Audio is split into:
|
||||
|
||||
- `music`
|
||||
- `sfx`
|
||||
- `dialogue`
|
||||
|
||||
Runtime dialogue data lives under:
|
||||
|
||||
```txt
|
||||
public/sounds/dialogue/
|
||||
```
|
||||
|
||||
The current subtitle model is one SRT file per voice and language. A dialogue entry references one cue by `subtitleCueIndex`.
|
||||
|
||||
`src/utils/dialogues/playDialogue.ts` queues dialogue playback and synchronizes the active subtitle cue against the playing audio element.
|
||||
|
||||
Detailed audio notes live in `docs/technical/audio.md`.
|
||||
|
||||
## Cinematics
|
||||
|
||||
- `public/cinematics.json` is the runtime cinematic manifest.
|
||||
- `src/types/cinematics/cinematics.ts` contains cinematic manifest types.
|
||||
- `src/utils/cinematics/cinematicManifestValidation.ts` validates manifest shape at runtime.
|
||||
- `src/utils/cinematics/loadCinematicManifest.ts` loads `/cinematics.json`.
|
||||
- `src/world/GameCinematics.tsx` triggers cinematics that define a global `timecode`.
|
||||
- Cinematics use GSAP timelines to animate the active camera position and look target.
|
||||
- `dialogueCues` on a cinematic trigger dialogue IDs at times relative to the cinematic start.
|
||||
- `src/managers/stores/useGameStore.ts` exposes `isCinematicPlaying`, used to lock player input during cinematics.
|
||||
Runtime cinematic data lives in:
|
||||
|
||||
## Debug System
|
||||
```txt
|
||||
public/cinematics.json
|
||||
```
|
||||
|
||||
- Debug mode is enabled with `?debug`.
|
||||
- `src/utils/debug/Debug.ts` owns the `lil-gui` instance and debug controls.
|
||||
- `src/hooks/debug/useCameraMode.ts` and `src/hooks/debug/useSceneMode.ts` subscribe to debug state.
|
||||
- `src/components/debug/DebugPerf.tsx` lazily mounts `r3f-perf` in debug mode.
|
||||
- `src/components/ui/debug/DebugOverlayLayout.tsx` mounts the compact HTML debug overlay when enabled from `lil-gui`.
|
||||
- `src/components/ui/debug/GameStateDebugPanel.tsx` exposes current game state, main/sub-state switching, previous/next step controls, and reset.
|
||||
- `src/components/ui/debug/HandTrackingDebugPanel.tsx` shows hand tracking status, usage, loaded glove model, hand count, and fist state while hand tracking is active.
|
||||
- `src/components/ui/SceneLoadingOverlay.tsx` displays the fullscreen loading state for 3D scenes, including the production game scene, debug physics scene, and editor scene.
|
||||
- `src/components/three/handTracking/HandTrackingGlove.tsx` places the rigged `gant_l` and `gant_r` models on detected hands in the debug physics scene.
|
||||
- `src/components/debug/scene/DebugHelpers.tsx` mounts debug helpers.
|
||||
- `src/components/debug/scene/DebugCameraControls.tsx` mounts the free debug camera.
|
||||
- `lil-gui` global debug controls include camera mode, scene mode, `R3F Perf`, and `Debug Overlay`; interaction-specific controls live in the `Interaction` folder.
|
||||
Cinematics support camera keyframes, GSAP timelines, optional dialogue cues, and `isCinematicPlaying` input locking. Current world integration mounts `GameCinematics` only during the outro state.
|
||||
|
||||
## 3D Component Domains
|
||||
## Hand Tracking
|
||||
|
||||
- `src/components/three/models/` contains reusable model helpers such as `ExplodableModel`.
|
||||
- `src/components/three/interaction/` contains reusable interaction wrappers such as `InteractableObject`, `TriggerObject`, and `GrabbableObject`.
|
||||
- `src/components/three/handTracking/` contains R3F hand tracking debug models such as the glove overlays.
|
||||
- `src/components/three/gameplay/` contains the reusable production `RepairGame` flow, repair case, repair steps, and repair prompt components.
|
||||
- `src/components/three/world/` contains reusable world/environment objects such as `SkyModel`.
|
||||
Hand tracking can use:
|
||||
|
||||
- local Python backend over WebSocket
|
||||
- browser-side MediaPipe through `@mediapipe/tasks-vision`
|
||||
|
||||
Important files:
|
||||
|
||||
- `src/providers/gameplay/HandTrackingProvider.tsx`
|
||||
- `src/hooks/handTracking/useRemoteHandTracking.ts`
|
||||
- `src/hooks/handTracking/useBrowserHandTracking.ts`
|
||||
- `src/hooks/handTracking/useBothFistsHold.ts`
|
||||
- `src/components/three/handTracking/HandTrackingGlove.tsx`
|
||||
- `backend/main.py`
|
||||
|
||||
Hand tracking is activated lazily. In production it is enabled during repair steps that need hand input. In debug physics mode it is enabled when interaction context makes hand input useful.
|
||||
|
||||
Detailed hand-tracking notes live in `docs/technical/hand-tracking.md`.
|
||||
|
||||
## Editor System
|
||||
|
||||
- `src/pages/editor/page.tsx` is the route-level editor page for `/editor`.
|
||||
- `src/components/editor/EditorControls.tsx` renders the HTML editor control panel.
|
||||
- `src/components/editor/EditorDialogueManifestPanel.tsx` edits `public/sounds/dialogue/dialogues.json`.
|
||||
- `src/components/editor/EditorCinematicManifestPanel.tsx` edits `public/cinematics.json`.
|
||||
- `src/components/editor/EditorSrtPanel.tsx` renders the dialogue SRT editor inside the editor control panel.
|
||||
- `src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, shortcuts, and map rendering.
|
||||
- `src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||
- `src/controls/editor/FlyController.tsx` provides player-style editor navigation.
|
||||
- `src/hooks/editor/useEditorSceneData.ts` loads scene data and handles folder upload fallback.
|
||||
- `src/hooks/editor/useEditorHistory.ts` owns editor undo and redo state.
|
||||
- `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing.
|
||||
- `src/utils/map/loadMapSceneData.ts` is shared by the game scene and editor to load `public/map.json` and resolve model URLs.
|
||||
- `src/types/editor/editor.ts` contains the shared `MapNode`, `SceneData`, and `TransformMode` types.
|
||||
- `src/types/gameplay/repairMission.ts` contains shared repair mission ids, mission steps, and guards used across store, config, debug UI, and gameplay components.
|
||||
The editor route is:
|
||||
|
||||
```txt
|
||||
/editor
|
||||
```
|
||||
|
||||
Important editor files:
|
||||
|
||||
- `src/pages/editor/page.tsx`
|
||||
- `src/components/editor/EditorControls.tsx`
|
||||
- `src/components/editor/scene/EditorScene.tsx`
|
||||
- `src/components/editor/scene/EditorMap.tsx`
|
||||
- `src/components/editor/EditorDialogueManifestPanel.tsx`
|
||||
- `src/components/editor/EditorCinematicManifestPanel.tsx`
|
||||
- `src/components/editor/EditorSrtPanel.tsx`
|
||||
- `src/hooks/editor/useEditorSceneData.ts`
|
||||
- `src/hooks/editor/useEditorHistory.ts`
|
||||
- `src/controls/editor/FlyController.tsx`
|
||||
|
||||
The editor shares `MapNode` data with the runtime map loader.
|
||||
|
||||
Local save endpoints live in `vite.config.ts`:
|
||||
|
||||
- `POST /api/save-map`
|
||||
- `POST /api/save-srt`
|
||||
- `GET /api/validate-dialogues`
|
||||
- `POST /api/save-dialogues`
|
||||
- `POST /api/save-cinematics`
|
||||
|
||||
These are Vite dev-server helpers, not production backend APIs.
|
||||
|
||||
Detailed editor notes live in `docs/technical/editor.md`.
|
||||
|
||||
## Documentation System
|
||||
|
||||
The docs route uses:
|
||||
|
||||
- `src/components/docs/DocsLayout.tsx`
|
||||
- `src/components/docs/DocsDocument.tsx`
|
||||
- `src/data/docs/docsSections.ts`
|
||||
- `src/routes/DocsRoute.tsx`
|
||||
- `src/pages/docs/**/page.tsx`
|
||||
|
||||
Docs pages import Markdown files with `?raw` and render them through `react-markdown` plus `remark-gfm`.
|
||||
|
||||
## 3D Component Domains
|
||||
|
||||
`src/components/three/` is organized by domain:
|
||||
|
||||
- `gameplay`: repair-game flow and repair components
|
||||
- `handTracking`: glove overlays
|
||||
- `interaction`: trigger/grab/focus wrappers
|
||||
- `models`: animated, simple, and explodable model helpers
|
||||
- `world`: world/environment objects
|
||||
|
||||
## Map Data
|
||||
|
||||
- `public/map.json` is expected to be a `MapNode[]`.
|
||||
- Each map node `name` maps to `public/models/{name}/model.glb` when available, with `public/models/{name}/model.gltf` kept as fallback.
|
||||
- The editor renders a fallback cube for missing models.
|
||||
- The game scene renders fallback cubes for nodes whose model cannot be resolved.
|
||||
- The game scene currently uses `terrain` as the collision source for the player octree. Additional collision nodes should be explicit lightweight collision assets, not arbitrary visible decoration models.
|
||||
Runtime map data:
|
||||
|
||||
```txt
|
||||
public/map.json
|
||||
```
|
||||
|
||||
Expected shape:
|
||||
|
||||
```ts
|
||||
interface MapNode {
|
||||
name: string;
|
||||
type: string;
|
||||
position: [number, number, number];
|
||||
rotation: [number, number, number];
|
||||
scale: [number, number, number];
|
||||
}
|
||||
```
|
||||
|
||||
Each `name` maps to:
|
||||
|
||||
```txt
|
||||
public/models/{name}/model.glb
|
||||
public/models/{name}/model.gltf
|
||||
```
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- The repository is a prototype, not the full intended game runtime.
|
||||
- `src/world/debug/TestMap.tsx` is part of the active scene composition.
|
||||
- There is no central gameplay orchestrator such as `GameManager`.
|
||||
- Mission state exists in Zustand and the repair flow is implemented as a prototype for the current repair missions.
|
||||
- Cinematics and dialogues exist as prototype timecode-driven systems; dialogue branching and broader gameplay orchestration are still limited.
|
||||
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
|
||||
- Editor save-to-server is implemented as a Vite dev-server plugin, not a production backend API.
|
||||
- The repository is still a prototype.
|
||||
- There is no central production `GameManager`.
|
||||
- The repair game is implemented, but broader mission orchestration is still light.
|
||||
- `useRepairMovementLocked()` currently returns `false`, so repair movement lock is disabled even though the rule and UI component exist.
|
||||
- The repair-runtime setting is stored in settings but not consumed by the repair-game implementation.
|
||||
- Player collision and Rapier gameplay physics are separate systems.
|
||||
- Editor persistence is local development tooling only.
|
||||
- Debug systems are still part of active scene composition and should remain easy to identify.
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
# Audio Technical Notes
|
||||
|
||||
This document describes the audio systems that exist in the current codebase.
|
||||
|
||||
## Scope
|
||||
|
||||
Audio is currently split into three runtime categories:
|
||||
|
||||
- `music`: looped background music
|
||||
- `dialogue`: spoken dialogue audio linked to subtitles
|
||||
- `sfx`: one-shot interaction and feedback sounds
|
||||
|
||||
The shared runtime service is `src/managers/AudioManager.ts`. User-facing volume settings live in `src/managers/stores/useSettingsStore.ts` and are forwarded to `AudioManager` by category.
|
||||
|
||||
## AudioManager
|
||||
|
||||
`AudioManager` is a singleton side-effect service. It owns browser audio elements, category volumes, pooled one-shot sounds, music playback, and stereo panning for one-shot sounds.
|
||||
|
||||
Supported public methods:
|
||||
|
||||
- `playMusic(path, volume)`: starts or updates a looped music track.
|
||||
- `stopMusic()`: stops the active music track.
|
||||
- `playSound(path, volume, options)`: plays a pooled one-shot sound and returns its `HTMLAudioElement`.
|
||||
- `setCategoryVolume(category, volume)`: updates `music`, `sfx`, or `dialogue` volume.
|
||||
- `getCategoryVolume(category)`: reads the current category volume.
|
||||
- `destroy()`: stops music, clears pools, closes the audio context, and resets the singleton.
|
||||
|
||||
One-shot sounds are pooled by path with a maximum pool size per sound. If every element in a pool is busy, the pool grows until the limit, then recycles an existing element.
|
||||
|
||||
Browser autoplay restrictions are handled in `playMusic()`: if playback is blocked by the browser, the manager waits for a user `pointerdown` or `keydown`, then retries the music.
|
||||
|
||||
## Music
|
||||
|
||||
Runtime music is mounted by `src/world/GameMusic.tsx`.
|
||||
|
||||
Current behavior:
|
||||
|
||||
- `GameMusic` calls `AudioManager.getInstance().playMusic()` on mount.
|
||||
- The current music path is `/sounds/musique/test.mp3`.
|
||||
- The base music volume is `0.33` before category volume is applied.
|
||||
- On unmount, `GameMusic` calls `stopMusic()`.
|
||||
|
||||
Effective music volume is:
|
||||
|
||||
```txt
|
||||
base music volume * settings music volume
|
||||
```
|
||||
|
||||
Use `music` only for long-running looped background tracks. Do not use `playSound()` for music, because one-shot pooling is designed for short overlapping sounds.
|
||||
|
||||
## Sound Effects
|
||||
|
||||
SFX are short one-shot sounds. They should use `AudioManager.playSound()` with the default category or with `{ category: "sfx" }`.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
AudioManager.getInstance().playSound("/sounds/sfx/click.mp3", 0.8, {
|
||||
category: "sfx",
|
||||
pan: 0,
|
||||
});
|
||||
```
|
||||
|
||||
Useful options:
|
||||
|
||||
- `category`: `sfx` or `dialogue`; defaults to `sfx`.
|
||||
- `pan`: stereo panning from `-1` left to `1` right.
|
||||
- `playbackRate`: playback speed multiplier.
|
||||
|
||||
SFX volume is controlled by the settings menu through the `sfx` category volume.
|
||||
|
||||
## Dialogues
|
||||
|
||||
Runtime dialogue data lives under `public/sounds/dialogue/`.
|
||||
|
||||
```txt
|
||||
public/
|
||||
└── sounds/
|
||||
└── dialogue/
|
||||
├── dialogues.json
|
||||
└── subtitles/
|
||||
├── fr/
|
||||
│ ├── narrateur.srt
|
||||
│ ├── fermier.srt
|
||||
│ └── electricienne.srt
|
||||
└── en/
|
||||
├── narrateur.srt
|
||||
├── fermier.srt
|
||||
└── electricienne.srt
|
||||
```
|
||||
|
||||
The dialogue manifest shape is defined in `src/types/dialogues/dialogues.ts`.
|
||||
|
||||
Each dialogue entry contains:
|
||||
|
||||
- `id`: stable dialogue identifier
|
||||
- `voice`: voice group, currently `narrateur`, `fermier`, or `electricienne`
|
||||
- `audio`: runtime audio path
|
||||
- `subtitleCueIndex`: cue number inside that voice/language SRT file
|
||||
- `timecode`: optional global trigger time in seconds from scene start
|
||||
|
||||
Dialogues are played through `src/utils/dialogues/playDialogue.ts`.
|
||||
|
||||
Important functions:
|
||||
|
||||
- `playDialogueById(manifest, dialogueId)`: plays a dialogue from an already loaded manifest.
|
||||
- `queueDialogueById(manifest, dialogueId)`: queues dialogue playback so multiple requests do not overlap.
|
||||
- `playGameplayDialogueById(dialogueId)`: loads the gameplay manifest once and queues a dialogue by ID.
|
||||
- `clearQueuedDialogues()`: resolves pending dialogue requests and clears the queue.
|
||||
|
||||
Dialogue audio uses `AudioManager.playSound()` with `{ category: "dialogue" }`, so it follows the dialogue volume setting.
|
||||
|
||||
## Dialogue And SRT Link
|
||||
|
||||
The subtitle model is one SRT file per voice and language, not one SRT file per dialogue.
|
||||
|
||||
A dialogue chooses its subtitle by combining:
|
||||
|
||||
1. `voice`
|
||||
2. selected subtitle language from settings
|
||||
3. `subtitleCueIndex`
|
||||
|
||||
For example, this dialogue:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "narrateur_bienvenueaaltera",
|
||||
"voice": "narrateur",
|
||||
"audio": "/sounds/dialogue/narrateur/bienvenueaaltera.mp3",
|
||||
"subtitleCueIndex": 1
|
||||
}
|
||||
```
|
||||
|
||||
loads cue `1` from:
|
||||
|
||||
```txt
|
||||
public/sounds/dialogue/subtitles/fr/narrateur.srt
|
||||
```
|
||||
|
||||
when the subtitle language is French, or from:
|
||||
|
||||
```txt
|
||||
public/sounds/dialogue/subtitles/en/narrateur.srt
|
||||
```
|
||||
|
||||
when the subtitle language is English.
|
||||
|
||||
If the selected language is missing, the loader falls back to French. Missing English SRT files are warnings during validation, not runtime errors.
|
||||
|
||||
SRT timecodes are relative to the dialogue audio file. They are not relative to the game clock and not relative to a cinematic timeline.
|
||||
|
||||
## Subtitle Runtime
|
||||
|
||||
`playDialogueById()` loads the matching subtitle cue with `loadDialogueSubtitleCue()` before playing the audio.
|
||||
|
||||
While audio plays:
|
||||
|
||||
- `timeupdate` checks `audio.currentTime`
|
||||
- the active subtitle is written to `useSubtitleStore`
|
||||
- `src/components/ui/Subtitles.tsx` renders the current speaker and text
|
||||
- `ended` and `pause` clear the subtitle
|
||||
|
||||
The subtitle overlay respects settings from `useSettingsStore`, including visibility and selected language.
|
||||
|
||||
## Global Timecode Dialogues
|
||||
|
||||
`src/world/GameDialogues.tsx` loads the dialogue manifest and triggers entries that define `timecode`.
|
||||
|
||||
This is useful for simple global scene timing. It should not be used for dialogue that belongs to a cinematic. Cinematic-owned dialogue should be triggered by `dialogueCues` in `public/cinematics.json` instead, otherwise the same dialogue can play twice.
|
||||
|
||||
## Cinematic Dialogue Cues
|
||||
|
||||
`public/cinematics.json` can include `dialogueCues`.
|
||||
|
||||
Each cue contains:
|
||||
|
||||
- `time`: seconds relative to the cinematic start
|
||||
- `dialogueId`: ID from `dialogues.json`
|
||||
|
||||
`src/world/GameCinematics.tsx` uses those cues to play dialogue during camera timelines. This keeps camera movement and dialogue playback synchronized without relying on global scene time.
|
||||
|
||||
## Editor Tooling
|
||||
|
||||
The `/editor` route provides three audio-related tools:
|
||||
|
||||
- `Dialogues`: edits `public/sounds/dialogue/dialogues.json` and previews dialogue playback.
|
||||
- `SRT`: edits one SRT file at a time and validates dialogue assets.
|
||||
- `Cinematics`: links dialogue IDs to cinematic timelines through `dialogueCues`.
|
||||
|
||||
Dev-only Vite endpoints in `vite.config.ts` support local saves:
|
||||
|
||||
- `POST /api/save-dialogues`
|
||||
- `POST /api/save-srt`
|
||||
- `GET /api/validate-dialogues`
|
||||
- `POST /api/save-cinematics`
|
||||
|
||||
These endpoints are local development helpers. They are not production APIs.
|
||||
|
||||
## Validation
|
||||
|
||||
`GET /api/validate-dialogues` validates:
|
||||
|
||||
- manifest shape
|
||||
- referenced dialogue audio files
|
||||
- French SRT files
|
||||
- referenced subtitle cue indexes
|
||||
- optional English SRT files as warnings
|
||||
|
||||
Run validation after adding or renaming dialogue audio, changing cue indexes, or editing SRT files.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- There is no production persistence for audio manifests or SRT files.
|
||||
- Dialogue branching is not implemented.
|
||||
- Dialogue interruption and priority rules are minimal; playback is queue-based.
|
||||
- SRT editing is text-based and does not yet provide waveform editing.
|
||||
- Music currently supports one active looped track at a time.
|
||||
@@ -52,7 +52,7 @@ src/
|
||||
|
||||
## Responsibilities
|
||||
|
||||
`src/pages/editor/page.tsx` is the route-level composition component. It owns route-specific state such as selected object, hovered object, transform mode, and player-mode toggle.
|
||||
`src/pages/editor/page.tsx` is the route-level composition component. It owns route-specific state such as selected object, hovered object, transform mode, selection lock, player-mode toggle, cinematic preview requests, and editor scene loading state.
|
||||
|
||||
`src/hooks/editor/useEditorSceneData.ts` loads the default map data and handles folder uploads.
|
||||
|
||||
@@ -62,7 +62,7 @@ src/
|
||||
|
||||
`src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||
|
||||
`src/components/editor/EditorControls.tsx` renders the HTML control panel outside the canvas.
|
||||
`src/components/editor/EditorControls.tsx` renders the HTML control panel outside the canvas. The panel is organized into top-level `details` groups: `Editor`, `Cinematics`, `Dialogues`, and `SRT`.
|
||||
|
||||
`src/components/editor/EditorDialogueManifestPanel.tsx` renders the dialogue manifest editor. It loads `dialogues.json`, edits dialogue entries, previews selected dialogue playback, creates missing French SRT cues, and saves the manifest through a dev-server endpoint.
|
||||
|
||||
@@ -122,13 +122,17 @@ If `model.glb` and `model.gltf` are both missing, the editor renders a fallback
|
||||
2. `useEditorSceneData` calls `loadMapSceneData()`.
|
||||
3. `loadMapSceneData()` loads `/map.json` and available model URLs.
|
||||
4. If `/map.json` is missing, the page displays a folder-upload flow.
|
||||
5. `EditorScene` renders the grid, lights, camera controls, and map nodes.
|
||||
6. `EditorControls` exposes transform mode, history actions, export, save, and selection info.
|
||||
5. `EditorSceneLoadingTracker` uses drei `useProgress()` to update the fullscreen editor loading overlay while models load.
|
||||
6. `EditorScene` renders the grid, lights, camera controls, and map nodes inside `Suspense`.
|
||||
7. `EditorControls` exposes transform mode, history actions, export, save, JSON preview, selection lock, and the cinematic/dialogue/SRT editors.
|
||||
|
||||
## Controls
|
||||
|
||||
- Click: select a node.
|
||||
- `Esc`: clear selection.
|
||||
- Click empty space: clear selection.
|
||||
- Selection lock button: prevent object clicks, empty-space clicks, and `Esc` from changing the current selection.
|
||||
- Selection clear button: intentionally clear the current selection even when the lock is active.
|
||||
- `T`: translate mode.
|
||||
- `R`: rotate mode.
|
||||
- `S`: scale mode.
|
||||
@@ -147,6 +151,51 @@ The editor supports two output paths:
|
||||
|
||||
The dev-only `/api/save-map` endpoint is implemented by the Vite plugin in `vite.config.ts`. It writes to `public/map.json` and enforces a maximum payload size.
|
||||
|
||||
## Editor Loading Overlay
|
||||
|
||||
The editor uses `SceneLoadingOverlay` like the runtime scene. `EditorSceneLoadingTracker` lives in `src/pages/editor/page.tsx` and reads drei `useProgress()` inside the canvas.
|
||||
|
||||
The route tracks two loading phases:
|
||||
|
||||
- map JSON loading through `useEditorSceneData()`
|
||||
- model loading through `useProgress()`
|
||||
|
||||
The overlay is rendered outside the canvas so it remains visible while the R3F scene mounts. The scene itself is wrapped in `Suspense` with a `null` fallback; the visual feedback is handled by the overlay instead of by the canvas fallback.
|
||||
|
||||
## Panel Groups
|
||||
|
||||
`EditorControls` uses the local `EditorPanelGroup` helper to keep the side panel navigable as tools grow.
|
||||
|
||||
Current group order:
|
||||
|
||||
1. `Editor`
|
||||
2. `Cinematics`
|
||||
3. `Dialogues`
|
||||
4. `SRT`
|
||||
|
||||
Inside the `Editor` group, the section order is:
|
||||
|
||||
1. `Shortcuts`
|
||||
2. `Transform`
|
||||
3. `Selection`
|
||||
4. `View`
|
||||
5. `JSON`
|
||||
6. `File`
|
||||
|
||||
The `Shortcuts` group is nested and closed by default to reduce visual noise.
|
||||
|
||||
## Selection Lock
|
||||
|
||||
Selection lock is owned by `EditorPage` through `isSelectionLocked`.
|
||||
|
||||
The state is passed to:
|
||||
|
||||
- `EditorControls`, to render the lock/unlock button
|
||||
- `EditorScene`, to block `Esc` deselection when locked
|
||||
- `EditorMap`, to block object selection and empty-space deselection when locked
|
||||
|
||||
The clear button calls `onClearSelection` directly from `EditorControls`. This is intentionally separate from scene click behavior so the user always has an explicit way to clear the selection.
|
||||
|
||||
## Dialogue SRT Editing
|
||||
|
||||
Dialogue subtitle editing is part of the `/editor` side panel.
|
||||
|
||||
@@ -95,20 +95,11 @@ If any ray hits the object while the object is within `INTERACTION_RADIUS`, the
|
||||
|
||||
## Depth Handling
|
||||
|
||||
Because MediaPipe `z` is relative, the frontend captures the starting depth when the grab begins:
|
||||
Because MediaPipe `z` is relative and noisy, the current frontend does not use it as a direct world-depth controller for object grabbing.
|
||||
|
||||
```txt
|
||||
initialHandZ = hand.z
|
||||
initialHoldDistance = hit.distance
|
||||
```
|
||||
Instead, `GrabbableObject` computes a ray from the 2D hand center and moves the object toward a configurable hold distance in front of the active camera. That hold distance is shared with the mouse grab path and can be tuned in the debug GUI.
|
||||
|
||||
While holding, the object distance from the camera is adjusted by the change in hand depth:
|
||||
|
||||
```txt
|
||||
holdDistance = initialHoldDistance + (hand.z - initialHandZ) * sensitivity
|
||||
```
|
||||
|
||||
The final hold distance is clamped between the configured grab minimum and maximum distances to avoid unstable movement.
|
||||
This is less expressive than true depth-aware hand movement, but it is more stable for the current first-person prototype.
|
||||
|
||||
## UI And Debug
|
||||
|
||||
@@ -131,7 +122,7 @@ The glove models are intentionally smaller than the raw SVG overlay so they do n
|
||||
## Known Limitations
|
||||
|
||||
- Production usage is currently limited to repair mission steps that explicitly need hands.
|
||||
- MediaPipe depth is relative and can be noisy.
|
||||
- MediaPipe depth is relative and currently not used for stable object depth control.
|
||||
- The virtual hit zone is an approximation based on multiple raycasts, not a real 3D collider.
|
||||
- There is no smoothing layer for hand position or depth yet.
|
||||
- The SVG hand visualization is a fallback, not the primary display when glove models load correctly.
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# Interaction System Technical Notes
|
||||
|
||||
This document explains the shared trigger, grab, focus, and hand-grab system.
|
||||
|
||||
## Purpose
|
||||
|
||||
The app has several ways for the player to affect the 3D scene:
|
||||
|
||||
- press `E` on focused trigger objects
|
||||
- hold the primary mouse button on grabbable objects
|
||||
- close a tracked hand into a fist to grab hand-controlled objects
|
||||
- release objects and optionally snap them into target positions
|
||||
|
||||
The implementation keeps those rules in a reusable interaction layer so gameplay features such as the repair game do not each create their own input system.
|
||||
|
||||
## Main Files
|
||||
|
||||
| File | Responsibility |
|
||||
| --------------------------------------------------------- | ----------------------------------------------- |
|
||||
| `src/managers/InteractionManager.ts` | Shared interaction state and imperative actions |
|
||||
| `src/hooks/interaction/useInteraction.ts` | React subscription to the manager |
|
||||
| `src/components/three/interaction/InteractableObject.tsx` | Distance/raycast focus detection |
|
||||
| `src/components/three/interaction/TriggerObject.tsx` | Press-to-trigger wrapper |
|
||||
| `src/components/three/interaction/GrabbableObject.tsx` | Physics-backed grab and hand grab wrapper |
|
||||
| `src/components/ui/InteractPrompt.tsx` | HTML prompt for focused trigger interactions |
|
||||
| `src/world/player/PlayerController.tsx` | Keyboard/mouse input bridge |
|
||||
|
||||
## Architecture
|
||||
|
||||
The interaction system has three layers:
|
||||
|
||||
1. R3F objects detect focus and register handles.
|
||||
2. `InteractionManager` stores the current interaction snapshot.
|
||||
3. UI and player input read the snapshot and trigger the selected action.
|
||||
|
||||
This is intentionally not Zustand. Interaction focus and holding state are short-lived, frame-adjacent runtime state. A small singleton plus `useSyncExternalStore` is a better fit than putting high-frequency interaction details into the durable game progression store.
|
||||
|
||||
## Interaction Snapshot
|
||||
|
||||
The snapshot type lives in:
|
||||
|
||||
```txt
|
||||
src/types/interaction/interaction.ts
|
||||
```
|
||||
|
||||
```ts
|
||||
interface InteractionSnapshot {
|
||||
focused: InteractableHandle | null;
|
||||
nearby: boolean;
|
||||
holding: boolean;
|
||||
handHolding: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
- `focused`: the interactable currently aimed at by the camera ray
|
||||
- `nearby`: at least one interactable is within interaction radius
|
||||
- `holding`: mouse/player-controller grab is active
|
||||
- `handHolding`: hand-tracking grab is active
|
||||
|
||||
`nearby`, `holding`, and `handHolding` are also used by the hand-tracking provider to decide when webcam tracking should stay active in the debug physics scene.
|
||||
|
||||
## Focus Detection
|
||||
|
||||
Focus detection lives in:
|
||||
|
||||
```txt
|
||||
src/components/three/interaction/InteractableObject.tsx
|
||||
```
|
||||
|
||||
Each frame, it:
|
||||
|
||||
1. finds the interactable world position from its Rapier body or group transform
|
||||
2. checks distance from the camera
|
||||
3. marks the handle as nearby if it is inside radius
|
||||
4. raycasts from the camera forward direction
|
||||
5. sets the focused handle when the ray hits the object
|
||||
6. clears focus if the object is no longer nearby or no longer aimed at
|
||||
|
||||
This gives a simple first-person interaction model: the player must be close enough and looking at the object.
|
||||
|
||||
## Trigger Objects
|
||||
|
||||
Trigger implementation:
|
||||
|
||||
```txt
|
||||
src/components/three/interaction/TriggerObject.tsx
|
||||
```
|
||||
|
||||
`TriggerObject` wraps children in a fixed Rapier body and exposes a trigger handle.
|
||||
|
||||
When triggered, it can:
|
||||
|
||||
- play an optional SFX through `AudioManager`
|
||||
- call `onTrigger`
|
||||
- spawn an optional model at an offset
|
||||
|
||||
Typical users:
|
||||
|
||||
- repair-object inspection
|
||||
- repair-case open/fragment interaction
|
||||
- install target
|
||||
- completion target
|
||||
- debug scene trigger sphere
|
||||
|
||||
## Grabbable Objects
|
||||
|
||||
Grab implementation:
|
||||
|
||||
```txt
|
||||
src/components/three/interaction/GrabbableObject.tsx
|
||||
```
|
||||
|
||||
`GrabbableObject` wraps children in a dynamic Rapier body and exposes a grab handle.
|
||||
|
||||
Mouse/controller grab flow:
|
||||
|
||||
1. Player focuses the object.
|
||||
2. Mouse down calls `InteractionManager.pressInteract()`.
|
||||
3. The object enters holding mode.
|
||||
4. Each frame, velocity is pushed toward a hold target in front of the camera.
|
||||
5. Mouse up calls `releaseInteract()`.
|
||||
6. The object can snap to the nearest configured target.
|
||||
|
||||
Important tuning values live in:
|
||||
|
||||
```txt
|
||||
src/data/interaction/grabConfig.ts
|
||||
```
|
||||
|
||||
The debug GUI exposes hold stiffness, throw boost, and hold distance.
|
||||
|
||||
## Snap-To-Target
|
||||
|
||||
`GrabbableObject` supports:
|
||||
|
||||
- `snapTargets`
|
||||
- `snapRadius`
|
||||
- `snapDuration`
|
||||
- `onSnap`
|
||||
|
||||
On release, the object finds the nearest target inside `snapRadius`. If a target is found, GSAP animates the Rapier body translation to that target and calls `onSnap`.
|
||||
|
||||
The repair game uses this to place replacement parts and broken parts into case placeholders.
|
||||
|
||||
## Hand-Controlled Grab
|
||||
|
||||
If `handControlled` is true, `GrabbableObject` also reads:
|
||||
|
||||
```txt
|
||||
useHandTrackingSnapshot()
|
||||
```
|
||||
|
||||
Hand grab flow:
|
||||
|
||||
1. Find a detected hand where `hand.isFist` is true.
|
||||
2. Compute the visual center of the hand from landmark bounds.
|
||||
3. Convert that screen-space point to a camera ray.
|
||||
4. Raycast against the object.
|
||||
5. Use a small set of offset rays around the center to make hit detection more forgiving.
|
||||
6. If the object is in range and hit, enter `handHolding`.
|
||||
7. Move the object toward a hold target in front of the camera while the fist remains closed.
|
||||
8. When the fist opens or disappears, release and snap if possible.
|
||||
|
||||
This is an approximation, not a full 3D hand collider. It is a practical prototype compromise because MediaPipe gives normalized camera-space landmarks and relative depth, not stable world-space hand meshes.
|
||||
|
||||
## Player Input Bridge
|
||||
|
||||
The player controller owns raw keyboard and mouse input:
|
||||
|
||||
```txt
|
||||
src/world/player/PlayerController.tsx
|
||||
```
|
||||
|
||||
It calls:
|
||||
|
||||
- `interaction.pressInteract()` when `E` is pressed and the focused handle is a trigger
|
||||
- `interaction.pressInteract()` on mouse down when the focused handle is a grab
|
||||
- `interaction.releaseInteract()` on mouse up when a grab is active
|
||||
|
||||
Input is ignored while:
|
||||
|
||||
- the settings menu is open
|
||||
- a cinematic is playing
|
||||
|
||||
Movement lock is read separately from `useRepairMovementLocked`, but that hook currently returns `false` on this branch.
|
||||
|
||||
## UI Prompt
|
||||
|
||||
The prompt lives in:
|
||||
|
||||
```txt
|
||||
src/components/ui/InteractPrompt.tsx
|
||||
```
|
||||
|
||||
It appears only when:
|
||||
|
||||
- camera mode is `player`
|
||||
- a focused interaction exists
|
||||
- the player is not holding an object
|
||||
- the focused interaction is a trigger
|
||||
|
||||
The prompt does not appear for grab objects, because grabs are mouse/hand actions rather than `E` trigger actions.
|
||||
|
||||
## Debug Controls
|
||||
|
||||
Interaction debugging is split between:
|
||||
|
||||
- lil-gui `Interaction` folder for showing interaction spheres
|
||||
- lil-gui `GrabbableObject` folder for grab tuning
|
||||
- debug physics scene for live trigger/grab testing
|
||||
- hand-tracking debug panel for hand grab state
|
||||
|
||||
Use:
|
||||
|
||||
```txt
|
||||
http://localhost:5173/?debug
|
||||
```
|
||||
|
||||
Then switch the scene mode to `Physics` from lil-gui.
|
||||
|
||||
## Why This Architecture Works
|
||||
|
||||
The interaction layer separates concerns:
|
||||
|
||||
- R3F objects know their distance/raycast hit state.
|
||||
- The player controller owns input events.
|
||||
- UI only subscribes to a snapshot.
|
||||
- Gameplay objects receive semantic callbacks like `onTrigger`, `onSnap`, or `onPositionChange`.
|
||||
|
||||
This keeps the repair game focused on gameplay rules instead of low-level input plumbing.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Only one focused handle is stored at a time.
|
||||
- The focus rule is camera ray based, so side-facing interactions can feel strict without larger meshes or radii.
|
||||
- Hand grab uses screen-space raycasts, not physical hand colliders.
|
||||
- The manager is singleton-based, so tests must call `destroy()` or isolate state when needed.
|
||||
- `nearby` is boolean, not a list exposed to UI, so the current UI cannot rank multiple nearby objects.
|
||||
@@ -0,0 +1,365 @@
|
||||
# Repair Game Technical Notes
|
||||
|
||||
This document explains the implementation of the reusable repair-game flow.
|
||||
|
||||
## Purpose
|
||||
|
||||
The repair game is the current core gameplay loop. It gives three missions the same interaction structure while allowing mission-specific assets, broken parts, replacement choices, prompts, and timing to live in data.
|
||||
|
||||
Implemented missions:
|
||||
|
||||
| Mission | Object | Role |
|
||||
| -------- | ------------- | --------------------------------------------- |
|
||||
| `bike` | E-bike | Repair a damaged cooling core |
|
||||
| `pylone` | Power pylon | Restore relay/panel-like broken parts |
|
||||
| `ferme` | Vertical farm | Stabilize irrigation/sensor-like broken parts |
|
||||
|
||||
## Main Files
|
||||
|
||||
| File | Responsibility |
|
||||
| ---------------------------------------------- | ------------------------------------------------- |
|
||||
| `src/components/three/gameplay/RepairGame.tsx` | Orchestrates the repair step machine |
|
||||
| `src/data/gameplay/repairMissions.ts` | Mission-specific data |
|
||||
| `src/types/gameplay/repairMission.ts` | Mission ids, step ids, guards |
|
||||
| `src/managers/stores/useGameStore.ts` | Global progression and mission transitions |
|
||||
| `src/world/GameStageContent.tsx` | Production placement of the three repair missions |
|
||||
| `src/world/debug/TestMap.tsx` | Debug repair playground placement |
|
||||
|
||||
## State Machine
|
||||
|
||||
Repair mission steps are defined in:
|
||||
|
||||
```txt
|
||||
src/types/gameplay/repairMission.ts
|
||||
```
|
||||
|
||||
```txt
|
||||
locked -> waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done
|
||||
```
|
||||
|
||||
The practical flow is:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> locked
|
||||
locked --> waiting: mission unlocked
|
||||
waiting --> inspected: inspect mission object
|
||||
inspected --> fragmented: repair-case trigger or two-fists hold
|
||||
fragmented --> scanning: fragmentation timer
|
||||
scanning --> repairing: scan sequence complete
|
||||
repairing --> reassembling: install target validates
|
||||
reassembling --> done: reassembly timer
|
||||
done --> [*]: completion target calls completeMission
|
||||
```
|
||||
|
||||
There is no dedicated finite-state-machine library. The state machine is intentionally lightweight and distributed across:
|
||||
|
||||
- `MissionStep` union types
|
||||
- Zustand transition helpers
|
||||
- conditional rendering in `RepairGame`
|
||||
- callbacks passed to step components
|
||||
|
||||
For the current prototype, this is readable and low overhead. If mission rules become much more branched, a centralized mission orchestrator or FSM library would become more useful.
|
||||
|
||||
## Integration With Zustand
|
||||
|
||||
The durable state lives in:
|
||||
|
||||
```txt
|
||||
src/managers/stores/useGameStore.ts
|
||||
```
|
||||
|
||||
`RepairGame` reads:
|
||||
|
||||
- `mainState`
|
||||
- current step for its mission
|
||||
|
||||
`RepairGame` writes:
|
||||
|
||||
- `setMissionStep(mission, nextStep)`
|
||||
- `completeMission(mission)`
|
||||
|
||||
The important architectural choice is that reusable repair components do not call `setBikeState`, `setPyloneState`, or `setFermeState` directly. They use generic mission actions so the same component can run for all three missions.
|
||||
|
||||
## Data-Driven Mission Config
|
||||
|
||||
Mission variation lives in:
|
||||
|
||||
```txt
|
||||
src/data/gameplay/repairMissions.ts
|
||||
```
|
||||
|
||||
Each mission config defines:
|
||||
|
||||
- `id`
|
||||
- `label`
|
||||
- `description`
|
||||
- `modelPath`
|
||||
- optional `modelScale`
|
||||
- `stageUiPath`
|
||||
- `interactUiPath`
|
||||
- `brokenUiPath`
|
||||
- repair case transform
|
||||
- optional scan/reassembly timings
|
||||
- `requiredReplacementPartId`
|
||||
- `brokenParts`
|
||||
- `replacementParts`
|
||||
|
||||
The main benefit is that `RepairGame` stays generic. A mission can change broken nodes, replacement choices, or prompt videos without changing the orchestration component.
|
||||
|
||||
The tradeoff is that the config can grow complex. If one future mission needs very different rules, create a mission-specific component instead of forcing every exception into the shared config.
|
||||
|
||||
## Orchestration Component
|
||||
|
||||
`RepairGame.tsx` is a step router.
|
||||
|
||||
It:
|
||||
|
||||
1. receives a `mission` id and transform props
|
||||
2. gets `config = REPAIR_MISSIONS[mission]`
|
||||
3. subscribes to the active `mainState`
|
||||
4. subscribes to the current mission step
|
||||
5. preloads mission assets
|
||||
6. mounts the component for the active step
|
||||
7. stores local runtime state needed between steps
|
||||
|
||||
Local runtime state:
|
||||
|
||||
- `casePlaceholders`: placeholder transforms emitted by the repair case GLTF
|
||||
- `scannedBrokenParts`: output of the scan sequence used by the repair step
|
||||
|
||||
Those values are local because they are transient scene/runtime details. They do not need to persist globally in Zustand.
|
||||
|
||||
## Step Components
|
||||
|
||||
### Waiting
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
src/components/three/gameplay/RepairInspectionObject.tsx
|
||||
```
|
||||
|
||||
The mission object is rendered with a 3D prompt video and wrapped in an interaction trigger. Pressing `E` while focused moves the mission to `inspected`.
|
||||
|
||||
### Inspected
|
||||
|
||||
Files:
|
||||
|
||||
```txt
|
||||
src/components/three/gameplay/RepairMissionCase.tsx
|
||||
src/components/three/gameplay/RepairCaseModel.tsx
|
||||
src/hooks/gameplay/useRepairFragmentationInput.ts
|
||||
```
|
||||
|
||||
The repair case appears near the mission object. The player can:
|
||||
|
||||
- aim at the case and press `E`
|
||||
- hold both fists closed for one second when hand tracking is active
|
||||
|
||||
Both paths move to `fragmented`.
|
||||
|
||||
Important current detail: `useRepairMovementLocked()` currently returns `false`, so the movement-lock rule and indicator are present but disabled in the current branch.
|
||||
|
||||
### Fragmented
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
src/components/three/models/ExplodableModel.tsx
|
||||
```
|
||||
|
||||
The mission object is shown split apart. A timer then moves the mission to `scanning`.
|
||||
|
||||
The default delay comes from:
|
||||
|
||||
```txt
|
||||
REPAIR_FRAGMENTATION_SEQUENCE_SECONDS
|
||||
```
|
||||
|
||||
### Scanning
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
src/components/three/gameplay/RepairScanSequence.tsx
|
||||
```
|
||||
|
||||
The scan sequence:
|
||||
|
||||
- keeps the exploded model visible
|
||||
- receives model parts from `ExplodableModel`
|
||||
- advances an active part index over time
|
||||
- renders `RepairScanVisual` on the active part
|
||||
- reveals broken-part highlights when configured broken parts have been reached
|
||||
- returns `RepairScannedBrokenPart[]` when done
|
||||
|
||||
Broken-part lookup first tries `brokenParts[].nodeName`. If no configured node matches, it falls back to the first available exploded parts. This fallback is useful while GLTF node names are still unstable, but precise `nodeName` config is safer for production.
|
||||
|
||||
### Repairing
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
src/components/three/gameplay/RepairRepairingStep.tsx
|
||||
```
|
||||
|
||||
This is the densest gameplay step.
|
||||
|
||||
It renders:
|
||||
|
||||
- install target
|
||||
- placeholder markers
|
||||
- grabbable replacement parts
|
||||
- grabbable broken parts to store
|
||||
- placement feedback
|
||||
- ready-to-install prompt
|
||||
|
||||
Important local state:
|
||||
|
||||
- `placedPartIds`: replacement parts that snapped near a placeholder
|
||||
- `depositedBrokenPartIds`: broken parts stored in the case
|
||||
- `showBlockedInstallFeedback`: temporary visual feedback when install is attempted too early
|
||||
|
||||
Validation:
|
||||
|
||||
```txt
|
||||
correct replacement part placed
|
||||
AND every scanned broken part deposited
|
||||
```
|
||||
|
||||
Only then does the install target call `onRepair()` and move to `reassembling`.
|
||||
|
||||
### Reassembling
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
src/components/three/gameplay/RepairReassemblyStep.tsx
|
||||
```
|
||||
|
||||
The exploded model animates back into assembled form and completion particles play. A timer then moves the mission to `done`.
|
||||
|
||||
Mission configs can override the default reassembly duration.
|
||||
|
||||
### Done
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
src/components/three/gameplay/RepairCompletionStep.tsx
|
||||
```
|
||||
|
||||
The repaired object remains visible. The player validates the completion target, then:
|
||||
|
||||
1. the repair case closes
|
||||
2. the case plays its exit animation
|
||||
3. `completeMission(mission)` advances the global game progression
|
||||
|
||||
## Repair Case Details
|
||||
|
||||
The case model implementation lives in:
|
||||
|
||||
```txt
|
||||
src/components/three/gameplay/RepairCaseModel.tsx
|
||||
```
|
||||
|
||||
It handles:
|
||||
|
||||
- GLTF loading through `useLoggedGLTF`
|
||||
- clone creation through `useClonedObject`
|
||||
- pop-in animation
|
||||
- lid open/close animation
|
||||
- open/close SFX through `AudioManager`
|
||||
- proximity-based floating
|
||||
- small rotation wobble
|
||||
- exit animation
|
||||
- placeholder discovery
|
||||
|
||||
Placeholder discovery is data-friendly:
|
||||
|
||||
```txt
|
||||
placeholder_*
|
||||
```
|
||||
|
||||
Any GLTF node whose name starts with that prefix is exported to the repair step as a placement target. This lets artists move placeholder transforms in the model file without hard-coding every placement point in TypeScript.
|
||||
|
||||
## Interaction Dependencies
|
||||
|
||||
The repair game depends on the shared interaction layer:
|
||||
|
||||
- `RepairInspectionObject` uses `InteractableObject`
|
||||
- `RepairMissionCase` uses `TriggerObject`
|
||||
- `RepairRepairingStep` uses `GrabbableObject` and `TriggerObject`
|
||||
- completion uses `TriggerObject`
|
||||
|
||||
This keeps the repair game from owning raw keyboard or mouse listeners for every object. The player controller handles input, and interaction components decide what is focused.
|
||||
|
||||
## Hand Tracking Dependencies
|
||||
|
||||
Hand tracking participates in two places:
|
||||
|
||||
- `useRepairFragmentationInput` uses `useBothFistsHold`
|
||||
- `GrabbableObject` can be `handControlled`
|
||||
|
||||
`HandTrackingProvider` enables tracking during the repair steps that are expected to use hands:
|
||||
|
||||
```txt
|
||||
inspected
|
||||
repairing
|
||||
reassembling
|
||||
done
|
||||
```
|
||||
|
||||
This avoids keeping the webcam active for the whole game scene.
|
||||
|
||||
## Runtime Placement
|
||||
|
||||
Production placement lives in:
|
||||
|
||||
```txt
|
||||
src/world/GameStageContent.tsx
|
||||
```
|
||||
|
||||
Current positions:
|
||||
|
||||
```tsx
|
||||
<RepairGame mission="bike" position={[8, 0, -6]} />
|
||||
<RepairGame mission="pylone" position={[64, 0, -66]} />
|
||||
<RepairGame mission="ferme" position={[-24, 0, 42]} />
|
||||
```
|
||||
|
||||
Only the repair game whose `mission` matches `useGameStore().mainState` renders active content.
|
||||
|
||||
## Debug Placement
|
||||
|
||||
Debug placement lives in:
|
||||
|
||||
```txt
|
||||
src/world/debug/TestMap.tsx
|
||||
```
|
||||
|
||||
The debug scene mounts repair playground zones for all missions. Use `?debug`, switch to the physics scene in lil-gui, then use the game-state debug panel to activate the mission you want to test.
|
||||
|
||||
## Why This Is A Good Review Focus
|
||||
|
||||
This feature shows several important frontend/game architecture skills:
|
||||
|
||||
- state-driven scene composition
|
||||
- data-driven feature variation
|
||||
- React state for step-local runtime values
|
||||
- Zustand for durable game progression
|
||||
- R3F component boundaries
|
||||
- Rapier object interaction
|
||||
- hand tracking integration
|
||||
- audio feedback
|
||||
- GLTF traversal
|
||||
- graceful asset fallbacks
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Movement lock is currently disabled by an early `return false` in `useRepairMovementLocked`.
|
||||
- The repair-game runtime setting in the options menu is stored but not consumed by `RepairGame`.
|
||||
- Broken-part scan fallback can produce incorrect matches if GLTF node names are missing.
|
||||
- Mission progression is still prototype-level and not owned by a central `GameManager`.
|
||||
- The same repair flow covers all missions. Very different future missions may need dedicated components.
|
||||
@@ -0,0 +1,252 @@
|
||||
# Scene Runtime And Loading
|
||||
|
||||
This document explains how the playable route boots the 3D world, loads the map, gates gameplay readiness, and spawns the player.
|
||||
|
||||
## Purpose
|
||||
|
||||
The playable scene has heavy asynchronous work: map JSON, GLTF models, collision meshes, octree construction, Rapier stage content, audio, dialogues, and the player controller.
|
||||
|
||||
The current runtime avoids spawning the player too early. That matters because the player controller needs a ready octree, and the repair game needs the production stage to be mounted before the user starts interacting with objects.
|
||||
|
||||
## Entry Flow
|
||||
|
||||
```txt
|
||||
src/main.tsx
|
||||
-> src/App.tsx
|
||||
-> src/router.tsx
|
||||
-> src/pages/page.tsx
|
||||
-> HandTrackingProvider
|
||||
-> Canvas
|
||||
-> World
|
||||
-> DebugPerf
|
||||
-> GameUI
|
||||
-> SceneLoadingOverlay
|
||||
```
|
||||
|
||||
`HomePage` owns the visible loading state and passes `onLoadingStateChange` down to `World`.
|
||||
|
||||
The loading progress in `HomePage` is monotonic:
|
||||
|
||||
- if the scene is already ready, a late loading event is ignored
|
||||
- progress can only increase while the scene is booting
|
||||
|
||||
This prevents the overlay from jumping backward when nested loaders finish in a slightly different order.
|
||||
|
||||
## World Composition
|
||||
|
||||
`src/world/World.tsx` is the main scene composer.
|
||||
|
||||
Always-mounted systems:
|
||||
|
||||
- `Environment`
|
||||
- `Lighting`
|
||||
- debug helpers when `?debug` is active
|
||||
- optional hand-tracking glove overlays
|
||||
- optional debug camera controls
|
||||
|
||||
Game scene systems:
|
||||
|
||||
- `GameMap`
|
||||
- Rapier `Physics` wrapping `GameStageContent`
|
||||
- `GameMusic`
|
||||
- `GameDialogues`
|
||||
- `GameCinematics`, currently only in `mainState === "outro"`
|
||||
- `Player`
|
||||
|
||||
Debug physics scene systems:
|
||||
|
||||
- `TestMap`
|
||||
- `Player`
|
||||
|
||||
## Loading State Owner
|
||||
|
||||
The world loading gate lives in:
|
||||
|
||||
```txt
|
||||
src/hooks/world/useWorldSceneLoading.ts
|
||||
```
|
||||
|
||||
It tracks:
|
||||
|
||||
- `octree`: collision octree built from collision source meshes
|
||||
- `gameMapLoaded`: map data and visible map nodes settled
|
||||
- `gameStageLoaded`: Rapier gameplay stage mounted
|
||||
- `showGameStage`: true when the map is ready enough to mount gameplay content
|
||||
- `gameplayReady`: true when map, stage, and octree are all ready
|
||||
|
||||
The final game-scene readiness condition is:
|
||||
|
||||
```ts
|
||||
showGameStage && gameStageLoaded && octree !== null;
|
||||
```
|
||||
|
||||
The debug physics scene is ready when:
|
||||
|
||||
```ts
|
||||
octree !== null;
|
||||
```
|
||||
|
||||
## Map Loading
|
||||
|
||||
Map loading starts in:
|
||||
|
||||
```txt
|
||||
src/world/GameMap.tsx
|
||||
```
|
||||
|
||||
`GameMap` calls:
|
||||
|
||||
```txt
|
||||
src/utils/map/loadMapSceneData.ts
|
||||
```
|
||||
|
||||
That utility:
|
||||
|
||||
1. fetches `/map.json`
|
||||
2. validates it as a `MapNode[]`
|
||||
3. deduplicates model names
|
||||
4. checks `public/models/{name}/model.glb`
|
||||
5. falls back to `public/models/{name}/model.gltf`
|
||||
6. returns `{ mapNodes, models }`
|
||||
|
||||
If a model is missing, the map still renders a fallback cube. This keeps the scene inspectable while assets are incomplete.
|
||||
|
||||
## Model Settling
|
||||
|
||||
`GameMap` counts settled map nodes.
|
||||
|
||||
A node settles when:
|
||||
|
||||
- it has no model and renders a fallback cube
|
||||
- its GLTF model instance has mounted
|
||||
- a model error boundary catches a load/render error and renders fallback
|
||||
|
||||
This prevents `GameMapCollision` from building collision before the visible map has reached a stable state.
|
||||
|
||||
## Collision Loading
|
||||
|
||||
Collision loading lives in:
|
||||
|
||||
```txt
|
||||
src/world/GameMapCollision.tsx
|
||||
```
|
||||
|
||||
The current production collision source is intentionally small:
|
||||
|
||||
```ts
|
||||
const MAP_COLLISION_NODE_NAMES = new Set(["terrain"]);
|
||||
```
|
||||
|
||||
Only matching map nodes are loaded into the invisible collision group. Then:
|
||||
|
||||
```txt
|
||||
src/hooks/three/useOctreeGraphNode.ts
|
||||
```
|
||||
|
||||
builds the Three.js octree from that group and sends it back through `onOctreeReady`.
|
||||
|
||||
This is a performance choice. Building a player collision octree from every visible prop can overload the browser and make the scene fragile.
|
||||
|
||||
## Stage Loading
|
||||
|
||||
Production gameplay content is mounted by:
|
||||
|
||||
```txt
|
||||
src/world/GameStageContent.tsx
|
||||
```
|
||||
|
||||
`World` wraps it in Rapier `Physics`, but only after `GameMap` reports loaded:
|
||||
|
||||
```tsx
|
||||
{
|
||||
showGameStage ? (
|
||||
<Physics>
|
||||
<GameStageLoaded onLoaded={handleGameStageLoaded} />
|
||||
<GameStageContent />
|
||||
</Physics>
|
||||
) : null;
|
||||
}
|
||||
```
|
||||
|
||||
`GameStageLoaded` is a tiny component that calls `handleGameStageLoaded()` after mount. It gives the loading hook a clear signal that the Rapier stage has entered the scene graph.
|
||||
|
||||
## Player Spawn Gate
|
||||
|
||||
The player is spawned only when the active camera mode is not debug and the active scene is ready.
|
||||
|
||||
```ts
|
||||
const spawnPlayer =
|
||||
cameraMode !== "debug" &&
|
||||
(sceneMode === "game" ? gameplayReady : octree !== null);
|
||||
```
|
||||
|
||||
This avoids two common bugs:
|
||||
|
||||
- the player starts falling or clipping before collision is ready
|
||||
- gameplay starts while the map/stage is still mounting
|
||||
|
||||
The production player spawn uses:
|
||||
|
||||
```txt
|
||||
PLAYER_SPAWN_POSITION_GAME
|
||||
```
|
||||
|
||||
The debug physics scene uses:
|
||||
|
||||
```txt
|
||||
PLAYER_SPAWN_POSITION_PHYSICS
|
||||
```
|
||||
|
||||
## Audio And Narrative Mounting
|
||||
|
||||
`GameMusic`, `GameDialogues`, and `Player` mount together after `spawnPlayer` is true.
|
||||
|
||||
This means background music and global dialogue timecode processing do not start while the loading overlay is still preparing the scene.
|
||||
|
||||
`GameCinematics` is currently gated further:
|
||||
|
||||
```tsx
|
||||
{
|
||||
mainState === "outro" ? <GameCinematics /> : null;
|
||||
}
|
||||
```
|
||||
|
||||
So cinematic playback is part of the outro path today, not a global always-on system.
|
||||
|
||||
## Debug Modes
|
||||
|
||||
Debug is enabled with:
|
||||
|
||||
```txt
|
||||
http://localhost:5173/?debug
|
||||
```
|
||||
|
||||
`src/utils/debug/Debug.ts` provides:
|
||||
|
||||
- camera mode: `player` or `debug`
|
||||
- scene mode: `game` or `physics`
|
||||
- R3F perf toggle
|
||||
- debug overlay toggle
|
||||
- hand-tracking source
|
||||
- hand SVG visibility
|
||||
- interaction sphere visibility
|
||||
|
||||
Important current detail: the older boot flags such as `noMusic`, `noCinematics`, `noMap`, `noDialogues`, `noOctree`, and `noPlayer` are not part of the current `develop` runtime path.
|
||||
|
||||
## Why This Architecture Works
|
||||
|
||||
The runtime uses React composition as the scene orchestration layer:
|
||||
|
||||
- if JSX is mounted, the Three/Rapier object exists
|
||||
- if JSX is unmounted, the object leaves the scene
|
||||
- loading gates are explicit booleans instead of hidden timing assumptions
|
||||
|
||||
This keeps the prototype understandable while still preventing expensive systems from starting too early.
|
||||
|
||||
## Risks And Watch Points
|
||||
|
||||
- Loading progress is manually estimated, not measured from every asset byte.
|
||||
- The production collision source is currently only `terrain`; extra collision needs explicit lightweight nodes.
|
||||
- Rapier gameplay physics and player octree collision are separate systems and can diverge if future features assume they are the same world.
|
||||
- `GameCinematics` is not globally mounted anymore; docs or tests that expect intro cinematics to auto-run should be updated before relying on that path.
|
||||
- Scene readiness is stored in React state, so remounting the route restarts the loading flow.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Three Debugging
|
||||
|
||||
Use the dedicated debug mode when you need Chrome DevTools to step into Three.js internals.
|
||||
|
||||
```bash
|
||||
npm run dev:three-debug
|
||||
```
|
||||
|
||||
This mode aliases `three` to `node_modules/three/src/Three.js` and disables Vite dependency pre-bundling for Three. In DevTools, open `node_modules/three/src/renderers/WebGLRenderer.js` and place a breakpoint inside:
|
||||
|
||||
```js
|
||||
this.render = function (scene, camera) {
|
||||
```
|
||||
|
||||
Reload the page or trigger a frame. When the breakpoint hits, inspect `scene`, `camera`, renderer state, visible objects, matrices, materials, and `this.info.render`.
|
||||
|
||||
If DevTools still opens a bundled file, stop the dev server, clear Vite's cached deps, and restart:
|
||||
|
||||
```bash
|
||||
rm -rf node_modules/.vite
|
||||
npm run dev:three-debug
|
||||
```
|
||||
+141
-89
@@ -1,75 +1,85 @@
|
||||
# Zustand Game State
|
||||
# Zustand Stores
|
||||
|
||||
This document explains how Zustand is used in the current project.
|
||||
|
||||
## Why Zustand Exists Here
|
||||
|
||||
The project needs one shared source of truth for the player's progression through the experience.
|
||||
The project needs shared state that is durable enough to be read by multiple React and React Three Fiber systems.
|
||||
|
||||
The current progression is split into main states:
|
||||
Zustand is used for:
|
||||
|
||||
- game progression
|
||||
- settings
|
||||
- subtitle display
|
||||
|
||||
It is not used for high-frequency frame values. Values such as player velocity, temporary vectors, object positions during a grab, raycasts, and animation-loop data stay in refs or manager-local state.
|
||||
|
||||
## Store Locations
|
||||
|
||||
Current Zustand stores:
|
||||
|
||||
```txt
|
||||
src/managers/stores/useGameStore.ts
|
||||
src/managers/stores/useSettingsStore.ts
|
||||
src/managers/stores/useSubtitleStore.ts
|
||||
```
|
||||
|
||||
They are under `src/managers/stores/` because they are shared runtime state, not state owned by one visual component.
|
||||
|
||||
## Store Responsibilities
|
||||
|
||||
| Store | Responsibility |
|
||||
| ------------------ | ----------------------------------------------------------------- |
|
||||
| `useGameStore` | Durable game progression, mission steps, cinematic input lock |
|
||||
| `useSettingsStore` | Menu visibility, volumes, subtitle options, repair-runtime toggle |
|
||||
| `useSubtitleStore` | Currently displayed subtitle cue |
|
||||
|
||||
## Managers vs Stores
|
||||
|
||||
Managers own imperative runtime objects and side effects.
|
||||
|
||||
Examples:
|
||||
|
||||
- `AudioManager` owns audio elements, music playback, sound pools, category volumes, and optional panner nodes.
|
||||
- `InteractionManager` owns transient interaction handles and input-oriented focus/holding state.
|
||||
|
||||
Stores own durable shared state:
|
||||
|
||||
- current game phase
|
||||
- mission sub-step
|
||||
- progression flags
|
||||
- settings values
|
||||
- currently displayed subtitle cue
|
||||
|
||||
Rule of thumb:
|
||||
|
||||
- manager = runtime objects, side effects, frame-adjacent imperative logic
|
||||
- store = shared state that UI, world, or gameplay components need to subscribe to
|
||||
|
||||
## Game Store Shape
|
||||
|
||||
`useGameStore` exposes the main game progression.
|
||||
|
||||
Main states:
|
||||
|
||||
| Main state | Role |
|
||||
| ---------- | ------------------------------- |
|
||||
| `intro` | Onboarding and opening sequence |
|
||||
| `bike` | E-bike repair sequence |
|
||||
| `pylone` | Power grid sequence |
|
||||
| `ferme` | Vertical farm sequence |
|
||||
| `pylone` | Power pylon repair sequence |
|
||||
| `ferme` | Vertical farm repair sequence |
|
||||
| `outro` | Ending sequence |
|
||||
|
||||
Each main state can also own smaller sub state, such as the current mission step, dialogue audio, or completion flags.
|
||||
Other important state:
|
||||
|
||||
Zustand is useful because React and React Three Fiber components can subscribe only to the state slice they need. When that slice changes, only the subscribed components re-render.
|
||||
- `isCinematicPlaying`
|
||||
- `intro`
|
||||
- `bike`
|
||||
- `pylone`
|
||||
- `ferme`
|
||||
- `outro`
|
||||
|
||||
## Store Location
|
||||
|
||||
The game progression store lives here:
|
||||
|
||||
```txt
|
||||
src/managers/stores/useGameStore.ts
|
||||
```
|
||||
|
||||
The store is placed under `src/managers/stores/` because it belongs to the gameplay orchestration layer, not to a specific visual component.
|
||||
|
||||
## Managers vs Store
|
||||
|
||||
Managers are responsible for local runtime objects and imperative behavior.
|
||||
|
||||
Examples:
|
||||
|
||||
- `AudioManager` owns audio elements and sound pools.
|
||||
- `InteractionManager` owns transient interaction handles and input-oriented behavior.
|
||||
|
||||
Managers can read from or write to the Zustand store when their local behavior needs to affect global gameplay progression.
|
||||
|
||||
The Zustand store is responsible for durable global state:
|
||||
|
||||
- current main state
|
||||
- mission sub state
|
||||
- progression flags
|
||||
- dialogue/audio references
|
||||
- state transitions
|
||||
|
||||
Rule of thumb:
|
||||
|
||||
- manager = runtime objects, side effects, and local imperative logic
|
||||
- store = global gameplay state that UI or world components can subscribe to
|
||||
|
||||
## Current Shape
|
||||
|
||||
The store exposes:
|
||||
|
||||
- `mainState`: the active game phase
|
||||
- `missionFlow`: intro and mission 2 prototype state
|
||||
- `intro`: intro-specific state
|
||||
- `bike`: e-bike mission state
|
||||
- `pylone`: power grid mission state
|
||||
- `ferme`: farm mission state
|
||||
- `outro`: ending state
|
||||
- actions for direct updates and progression updates
|
||||
|
||||
The `missionFlow` slice contains the prototype step, player name, movement lock, city activity flag, and temporary dialog message. It is in the main game store because it is global gameplay state used by UI, world components, and the player controller.
|
||||
|
||||
The mission steps currently use this sequence:
|
||||
Mission steps:
|
||||
|
||||
```ts
|
||||
"locked" |
|
||||
@@ -82,6 +92,8 @@ The mission steps currently use this sequence:
|
||||
"done";
|
||||
```
|
||||
|
||||
`isCinematicPlaying` is read by `PlayerController` to ignore player input while camera timelines are active.
|
||||
|
||||
## Reading State In Components
|
||||
|
||||
Use selectors to read only what the component needs.
|
||||
@@ -98,7 +110,7 @@ export function Example(): React.JSX.Element {
|
||||
|
||||
This is better than reading the whole store, because the component re-renders only when `mainState` changes.
|
||||
|
||||
## Updating State
|
||||
## Updating Game State
|
||||
|
||||
Prefer explicit actions from the store.
|
||||
|
||||
@@ -116,9 +128,15 @@ const setMainState = useGameStore((state) => state.setMainState);
|
||||
setMainState("bike");
|
||||
```
|
||||
|
||||
Direct setters are useful for debug panels, but production gameplay should prefer business actions such as `advanceGameState`, `completeBike`, or `completePylone`.
|
||||
Direct setters are useful for debug panels, but production gameplay should prefer business actions such as:
|
||||
|
||||
Mission gameplay that can target `bike`, `pylone`, or `ferme` should prefer the generic mission actions:
|
||||
- `advanceGameState`
|
||||
- `completeBike`
|
||||
- `completePylone`
|
||||
- `completeFerme`
|
||||
- `completeMission`
|
||||
|
||||
Mission gameplay that can target `bike`, `pylone`, or `ferme` should prefer generic mission actions:
|
||||
|
||||
```ts
|
||||
const setMissionStep = useGameStore((state) => state.setMissionStep);
|
||||
@@ -128,42 +146,71 @@ setMissionStep("bike", "inspected");
|
||||
completeMission("bike");
|
||||
```
|
||||
|
||||
This keeps reusable gameplay components such as repair flows from duplicating mission-specific branches like `setBikeState`, `setPyloneState`, and `setFermeState`.
|
||||
This keeps reusable gameplay components such as `RepairGame` from duplicating mission-specific branches like `setBikeState`, `setPyloneState`, and `setFermeState`.
|
||||
|
||||
## Settings Store
|
||||
|
||||
`useSettingsStore` owns player-facing settings and forwards audio volume changes to `AudioManager`.
|
||||
|
||||
State:
|
||||
|
||||
- `isSettingsMenuOpen`
|
||||
- `musicVolume`
|
||||
- `sfxVolume`
|
||||
- `dialogueVolume`
|
||||
- `subtitlesEnabled`
|
||||
- `subtitleLanguage`
|
||||
- `repairRuntime`
|
||||
|
||||
Audio setters clamp values between `0` and `1`, then call:
|
||||
|
||||
```ts
|
||||
AudioManager.getInstance().setCategoryVolume(category, nextVolume);
|
||||
```
|
||||
|
||||
This keeps UI state and browser audio state synchronized.
|
||||
|
||||
Current caveat: `repairRuntime` is stored and displayed in the settings menu, but the repair game does not consume it yet. Treat it as a staged architecture hook rather than an active runtime switch.
|
||||
|
||||
## Subtitle Store
|
||||
|
||||
`useSubtitleStore` is intentionally tiny.
|
||||
|
||||
State/actions:
|
||||
|
||||
- `activeSubtitle`
|
||||
- `setActiveSubtitle`
|
||||
- `clearActiveSubtitle`
|
||||
|
||||
`playDialogueById()` writes to this store while dialogue audio plays. `Subtitles` reads from it and respects `useSettingsStore().subtitlesEnabled`.
|
||||
|
||||
## World Integration
|
||||
|
||||
`src/world/GameStageContent.tsx` subscribes to `mainState` and mounts stage-specific content.
|
||||
`src/world/GameStageContent.tsx` subscribes to `mainState` and mounts the repair-game content.
|
||||
|
||||
For repair missions, it mounts the reusable `RepairGame` component with a mission id:
|
||||
Current production repair placement:
|
||||
|
||||
```tsx
|
||||
<RepairGame mission="bike" position={[8, 0, -6]} />
|
||||
<RepairGame mission="pylone" position={[64, 0, -66]} />
|
||||
<RepairGame mission="ferme" position={[-24, 0, 42]} />
|
||||
```
|
||||
|
||||
`RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep` and `completeMission`. Shared repair ids, mission steps, and runtime guards live in `src/types/gameplay/repairMission.ts` so static mission config does not depend on the Zustand store. The production repair flow currently supports `waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission` state transitions.
|
||||
`RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep` and `completeMission`.
|
||||
|
||||
Mission-specific behavior stays in `src/data/gameplay/repairMissions.ts`: each mission can define its broken nodes, placeholder targets, scan duration, and reassembly duration without adding mission branches to `RepairGame`.
|
||||
Shared repair ids, mission steps, and runtime guards live in:
|
||||
|
||||
The intro and mission 2 prototype flow is documented separately in `docs/technical/mission-flow.md`. It intentionally uses the same `useGameStore` source of truth instead of a dedicated `GameStepManager` or a second Zustand store.
|
||||
|
||||
That means the scene can progressively move toward this pattern:
|
||||
|
||||
```tsx
|
||||
switch (mainState) {
|
||||
case "intro":
|
||||
return <IntroContent />;
|
||||
case "bike":
|
||||
return <BikeContent />;
|
||||
case "pylone":
|
||||
return <PyloneContent />;
|
||||
case "ferme":
|
||||
return <FarmContent />;
|
||||
case "outro":
|
||||
return <OutroContent />;
|
||||
}
|
||||
```txt
|
||||
src/types/gameplay/repairMission.ts
|
||||
```
|
||||
|
||||
In React Three Fiber, mounting and unmounting JSX controls what appears in the Three.js scene. When a state-specific component disappears from JSX, React removes it from the scene.
|
||||
Mission-specific behavior stays in:
|
||||
|
||||
```txt
|
||||
src/data/gameplay/repairMissions.ts
|
||||
```
|
||||
|
||||
That lets the repair flow stay reusable while each mission defines its own model, broken parts, replacement parts, prompts, and timing.
|
||||
|
||||
## UI Integration
|
||||
|
||||
@@ -171,14 +218,16 @@ In React Three Fiber, mounting and unmounting JSX controls what appears in the T
|
||||
|
||||
Current overlays:
|
||||
|
||||
- `DebugOverlayLayout`: debug-only overlay shown with `?debug`, including the `GameStateDebugPanel` progression panel
|
||||
- `GameStateDebugPanel`: compact debug UI for viewing and switching main/sub states, stepping backward or forward, and resetting the store
|
||||
- `DebugOverlayLayout`: debug-only overlay shown with `?debug`
|
||||
- `GameStateDebugPanel`: compact debug UI for viewing and switching main/sub states
|
||||
- `Crosshair`: player aiming helper
|
||||
- `InteractPrompt`: interaction prompt
|
||||
- `RepairMovementLockIndicator`: player-facing indicator shown while repair steps temporarily disable movement
|
||||
- Mission flow overlays such as `IntroUI`, `BienvenueDisplay`, and `DialogMessage` are mounted by `src/pages/page.tsx` because they are route-level HTML overlays rather than persistent game HUD elements.
|
||||
- `RepairMovementLockIndicator`: indicator intended for repair movement lock
|
||||
- `HandTrackingVisualizer`: hand tracking SVG fallback/debug visualization
|
||||
- `Subtitles`: active dialogue subtitle overlay
|
||||
- `GameSettingsMenu`: options menu and settings controls
|
||||
|
||||
`src/pages/page.tsx` should stay thin and mount the canvas, persistent `GameUI`, and route-level overlays.
|
||||
Current caveat: `useRepairMovementLocked()` returns `false` immediately on the current branch, so the movement-lock rule and indicator exist but are disabled at runtime.
|
||||
|
||||
## Regression Rules
|
||||
|
||||
@@ -188,7 +237,10 @@ Current overlays:
|
||||
- Keep gameplay transitions inside store actions when possible.
|
||||
- Keep debug-only controls behind `?debug`.
|
||||
- Add new state only when a real runtime feature needs it.
|
||||
- Keep settings side effects, such as audio category updates, inside settings actions rather than spreading them across UI components.
|
||||
|
||||
## Next Steps
|
||||
|
||||
Move repair validation into mission data once each mission has distinct broken module nodes, replacement assets, and completion events.
|
||||
- Decide whether `repairRuntime` should be removed, implemented, or clearly labeled as experimental.
|
||||
- Re-enable or remove the repair movement-lock rule depending on desired gameplay.
|
||||
- Move broader mission orchestration into a clearer layer if intro, mission, dialogue, and cinematic branching grows.
|
||||
|
||||
Reference in New Issue
Block a user