Compare commits
284 Commits
b523e98bea
...
feat/intro
| Author | SHA1 | Date | |
|---|---|---|---|
| 988c8305bc | |||
| 5ee52ec752 | |||
| 3ece1d76de | |||
| 3222d2ed3d | |||
| 41c38a35b2 | |||
| 6399a2f89f | |||
| f5d3d080c8 | |||
| cac66ebaee | |||
| c155d847e9 | |||
| 7785a6c9d7 | |||
| 48f8de5ea5 | |||
| 73fc9ec677 | |||
| 90cd4d40cc | |||
| 5c68e4cd79 | |||
| f567540f22 | |||
| 688302d985 | |||
| f9d7c3f00e | |||
| 28c6ef199f | |||
| ff79448ce8 | |||
| 2b3ccaf67d | |||
| 122af7bd9a | |||
| ae34dc38ed | |||
| 1cecead474 | |||
| ceffedf684 | |||
| 43b64172ea | |||
| aa2250aba1 | |||
| 544e540c6a | |||
| 832f8b59be | |||
| 700c088c48 | |||
| 490f9627cc | |||
| 8d197ba26b | |||
| eab552a09b | |||
| 8c54451b2e | |||
| c09818aa67 | |||
| e65ad62b76 | |||
| b04aa95966 | |||
| cfb761e66c | |||
| 3bef55e12f | |||
| 15361db203 | |||
| 45fc8e9a83 | |||
| ffca1e9e5f | |||
| 2c3f0db65b | |||
| 91ebea8d99 | |||
| e05c67ee73 | |||
| 5c8c35cc72 | |||
| 711c336f1e | |||
| 12c96b43c1 | |||
| 2bb2fff310 | |||
| 052ab34740 | |||
| f7b968abe7 | |||
| ff21b80d57 | |||
| 8088e67625 | |||
| 6f5137229d | |||
| 6d3a8fde52 | |||
| 26a9c1c4d4 | |||
| f91756bc27 | |||
| e9875206ff | |||
| 841f96f3fd | |||
| 00b1780b10 | |||
| c81bacc208 | |||
| 49d1a7324c | |||
| 131fe39311 | |||
| 3c72298a45 | |||
| 49e96c7f5a | |||
| e5aeab6534 | |||
| cec4d6ad0d | |||
| 202ac4628d | |||
| 6a4d0f7eb1 | |||
| 40368c22fc | |||
| 788e9f0fb3 | |||
| 7a439d0879 | |||
| 9f5c105c1b | |||
| 72f8acb61c | |||
| 3791ab71cd | |||
| 1ad4a164ea | |||
| cc8ea20536 | |||
| bd14042ca0 | |||
| 7036b7b0f9 | |||
| f4635def91 | |||
| e16d6e15fa | |||
| f9a6390260 | |||
| b521cdd361 | |||
| 4218fde63a | |||
| 32d644b09d | |||
| c2ba26ca86 | |||
| 1b7813a5bb | |||
| f74a0c5eae | |||
| 41f7b2ad19 | |||
| a93d2dc92c | |||
| 4b7498ae13 | |||
| a34396b958 | |||
| 8cbf696b6e | |||
| 311c243506 | |||
| e8fb859f79 | |||
| 17836ec889 | |||
| 6101bca84f | |||
| 5889a325ec | |||
| 08f89c9de5 | |||
| 5ec10f4a35 | |||
| 71b46b5fea | |||
| 4a8a1368b2 | |||
| c9e5ce3279 | |||
| f55be58c0b | |||
| 48f2c9ef80 | |||
| 807503fde5 | |||
| 75b77a52bf | |||
| 916b5d304f | |||
| 3d81a9281a | |||
| 0c8b9070bb | |||
| 0fbf6bfa0e | |||
| 53fdf3cb1e | |||
| 8ef1da0e9a | |||
| 1c30b73253 | |||
| f5bc7cb08e | |||
| d9525b0aaf | |||
| 5b43f503ac | |||
| 974f1e33fb | |||
| ce67d07107 | |||
| 53add29a48 | |||
| 33524f8409 | |||
| e0eae67ace | |||
| 254311bddf | |||
| 6d9eac291e | |||
| e4f6ec211c | |||
| ead3634aab | |||
| 19a83982a9 | |||
| bebb9ac5a3 | |||
| d02ef54bdc | |||
| 3f3f623832 | |||
| 0256dfa812 | |||
| 5c688fdaf7 | |||
| 7a3baa4c0b | |||
| 95d9bd4f3e | |||
| eee69825c6 | |||
| 15c3d1858f | |||
| 7ee842c535 | |||
| d4f215a948 | |||
| 7bbcf4359e | |||
| eed0077dd1 | |||
| f5da2f4994 | |||
| ed60114d06 | |||
| 861a369776 | |||
| 96796bca65 | |||
| 8fa4b087ba | |||
| 9371c50110 | |||
| fb230911a7 | |||
| 457eebc0e3 | |||
| 03dfef4aad | |||
| 4bcdbef974 | |||
| 553dc6eb0a | |||
| 7ae45d4cfa | |||
| 27928b00a6 | |||
| ac7f60060c | |||
| fe662ebe7d | |||
| bdc06f772f | |||
| f7a589a11f | |||
| c71cd35f4d | |||
| 0cb5f57182 | |||
| 1d64582383 | |||
| 4d7d2efdcc | |||
| d438c02ad9 | |||
| eef39ab53d | |||
| 1a783f1867 | |||
| bb08054722 | |||
| 1625895708 | |||
| a87ffac63f | |||
| 44a77a0197 | |||
| f301a1e318 | |||
| 39fbf2333d | |||
| 5f018e6de7 | |||
| a14f776e5d | |||
| 8884edb281 | |||
| b9970c4e03 | |||
| 6a29cfdb0b | |||
| 7c7dbdb588 | |||
| 2696289483 | |||
| b79f5c9314 | |||
| 5265cdc7e5 | |||
| cfb1eaf39a | |||
| b1187b68ae | |||
| 9ac5844182 | |||
| 37eded8d7e | |||
| 92097e5256 | |||
| 01c583ba96 | |||
| f66609178b | |||
| 2783b13488 | |||
| 8b3f24b90b | |||
| 5173002283 | |||
| fd5294fb54 | |||
| 95e7e9c0a5 | |||
| 94b8dd0385 | |||
| ad6f1552c6 | |||
| d3ba5f6cfb | |||
| 05ef9e012a | |||
| aa8b31033f | |||
| 719ddef3ba | |||
| 462b80e1c2 | |||
| 90bd216efe | |||
| ba9833d407 | |||
| b736de5f25 | |||
| d482ae4634 | |||
| 7958b2c62a | |||
| d3a3d5eeca | |||
| dca8f5a0d3 | |||
| 8e08c3d5e0 | |||
| 882f3cc71b | |||
| 6c85485622 | |||
| a14ff9d913 | |||
| cc4c11f934 | |||
| 0a0519cead | |||
| 7d09c29828 | |||
| 28e3ac4c06 | |||
| cc78420d9c | |||
| 2747a95847 | |||
| d116f26a42 | |||
| 9f746aa7b0 | |||
| 847d6834fd | |||
| bd641328b0 | |||
| dbbb67e55a | |||
| 10dac2e56b | |||
| aaad1b5884 | |||
| bf98f028fe | |||
| bff24030ee | |||
| 6531d9b183 | |||
| 38fa9c1ff2 | |||
| e8f621d35f | |||
| aa211d16b7 | |||
| 44c27be640 | |||
| 8a58f75864 | |||
| 0c7d5b61ac | |||
| 5c5ba0937e | |||
| dc7de253de | |||
| 97e0606439 | |||
| a11db585de | |||
| 6d858cfa7d | |||
| 19bad2c8be | |||
| e01d6f27ba | |||
| 64b53a762d | |||
| 9818e719ce | |||
| 2251a81ac1 | |||
| eebeee9ed8 | |||
| 9c7c59973e | |||
| dee232ac45 | |||
| 2117146de5 | |||
| 632d4b5c95 | |||
| 291e747f7b | |||
| 48b53746d1 | |||
| ddde5e33f3 | |||
| b5f2b7c990 | |||
| bce1a05615 | |||
| 81515e3e31 | |||
| ed9582562a | |||
| 2c295fa7ee | |||
| 4a2f9cfb50 | |||
| d70de8c9a4 | |||
| f83df3dd21 | |||
| 5e528a90f9 | |||
| f3888ed0bf | |||
| b8f6fe59db | |||
| d7425ac17d | |||
| f8b2c180cd | |||
| c6b317448f | |||
| bed5010f41 | |||
| 1b1f2088aa | |||
| 18d0c2baa5 | |||
| 4aaff26e82 | |||
| 31c3e43698 | |||
| c5b04764ec | |||
| 1e444620c1 | |||
| eeca87dd0e | |||
| fde724f3f0 | |||
| d5a295e18f | |||
| 14626cd6b1 | |||
| 9c602cdc63 | |||
| fa8bc229c3 | |||
| 7139ae559e | |||
| 1c9d113050 | |||
| 70720fbdcf | |||
| cd7ba7503c | |||
| 2bd0fe5b96 | |||
| 037174a409 | |||
| 7de5678db1 | |||
| 1ca13ca13e | |||
| ca9abf3f3b |
+3
-2
@@ -11,7 +11,7 @@ You are working on **La Fabrik**, an interactive 3D web experience built with Re
|
|||||||
## Current Implementation
|
## Current Implementation
|
||||||
|
|
||||||
- Stack: React 19, Three.js, `@react-three/fiber`, `@react-three/drei`, `@react-three/rapier`, TypeScript, Vite
|
- Stack: React 19, Three.js, `@react-three/fiber`, `@react-three/drei`, `@react-three/rapier`, TypeScript, Vite
|
||||||
- No external global state library is used.
|
- Zustand is used for shared game progression state.
|
||||||
- Current singleton-style services are limited to:
|
- Current singleton-style services are limited to:
|
||||||
- `InteractionManager`
|
- `InteractionManager`
|
||||||
- `AudioManager`
|
- `AudioManager`
|
||||||
@@ -24,7 +24,8 @@ You are working on **La Fabrik**, an interactive 3D web experience built with Re
|
|||||||
|
|
||||||
## Current Architecture Rules
|
## Current Architecture Rules
|
||||||
|
|
||||||
- Scene objects live in `src/world/` and `src/components/3d/`.
|
- Scene objects live in `src/world/` and `src/components/three/`.
|
||||||
|
- Shared 3D components are grouped by domain under `src/components/three/models/`, `src/components/three/interaction/`, `src/components/three/gameplay/`, and `src/components/three/world/`.
|
||||||
- HTML overlays live in `src/components/ui/`.
|
- HTML overlays live in `src/components/ui/`.
|
||||||
- Shared static config lives in `src/data/`.
|
- Shared static config lives in `src/data/`.
|
||||||
- Debug tooling lives in `src/utils/debug/` and `src/hooks/debug/`.
|
- Debug tooling lives in `src/utils/debug/` and `src/hooks/debug/`.
|
||||||
|
|||||||
@@ -58,19 +58,18 @@ if (debug.active) {
|
|||||||
r3f-perf is loaded only in debug mode to avoid dependency issues in production:
|
r3f-perf is loaded only in debug mode to avoid dependency issues in production:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// src/utils/debug/DebugPerf.tsx
|
|
||||||
import { Suspense, lazy } from "react";
|
import { Suspense, lazy } from "react";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { useShowDebugPerf } from "@/hooks/debug/useShowDebugPerf";
|
||||||
|
|
||||||
const Perf = lazy(() => import("r3f-perf").then((m) => ({ default: m.Perf })));
|
const Perf = lazy(() => import("r3f-perf").then((m) => ({ default: m.Perf })));
|
||||||
|
|
||||||
export function DebugPerf() {
|
export function DebugPerf() {
|
||||||
const debug = Debug.getInstance();
|
const showDebugPerf = useShowDebugPerf();
|
||||||
if (!debug.active) return null;
|
if (!showDebugPerf) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Perf position="top-left" />
|
<Perf position="top-right" />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -89,6 +88,9 @@ Usage in Canvas:
|
|||||||
|
|
||||||
- All debug UI goes through `Debug.getInstance()` — never inline `if (isDev)` checks
|
- All debug UI goes through `Debug.getInstance()` — never inline `if (isDev)` checks
|
||||||
- r3f-perf is always lazy-imported, never a hard dependency in scene components
|
- r3f-perf is always lazy-imported, never a hard dependency in scene components
|
||||||
- Debug folders should be organized by domain (Lighting, PostFX, Player, Zone)
|
- Debug folders should be organized by domain (Lighting, Player, Zone, Interaction)
|
||||||
|
- Global debug controls include camera mode, scene mode, `R3F Perf`, and `Debug Overlay`
|
||||||
|
- Interaction-specific controls such as interaction spheres belong in the `Interaction` folder
|
||||||
|
- HTML debug panels should be grouped under `src/components/ui/debug/DebugOverlayLayout.tsx`
|
||||||
- Debug panel must not affect production builds — it simply doesn't mount when `?debug` is absent
|
- Debug panel must not affect production builds — it simply doesn't mount when `?debug` is absent
|
||||||
- Clean up debug folders in `destroy()` when relevant
|
- Clean up debug folders in `destroy()` when relevant
|
||||||
|
|||||||
+19
-16
@@ -28,14 +28,17 @@ export class SomeManager {
|
|||||||
|
|
||||||
## Managers in this project
|
## Managers in this project
|
||||||
|
|
||||||
| Manager | File | Role |
|
| Manager | File | Role |
|
||||||
| ------------------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
| -------------------- | ------------------------------------ | ----------------------------------------------------------------------------- |
|
||||||
| `GameManager` | `src/stateManager/GameManager.ts` | Single source of truth. Owns phase, zone, mission, input lock, dialogue. Has `subscribe()` + `getState()`. |
|
| `AudioManager` | `src/managers/AudioManager.ts` | Music and SFX playback. |
|
||||||
| `CinematicManager` | `src/stateManager/CinematicManager.ts` | GSAP timelines. Locks/unlocks input via GameManager. |
|
| `InteractionManager` | `src/managers/InteractionManager.ts` | Focus, nearby, trigger, grab, and hand-grab interaction state. |
|
||||||
| `AudioManager` | `src/stateManager/AudioManager.ts` | Music, SFX, spatial audio. Reads phase from GameManager. |
|
| `GameManager` | target-state only | Future single source of truth for phase, zone, mission, input lock, dialogue. |
|
||||||
| `ZoneManager` | `src/stateManager/ZoneManager.ts` | Zone entry/exit detection, LOD triggers. Notifies GameManager of zone changes. |
|
| `CinematicManager` | target-state only | Future GSAP timeline orchestrator. |
|
||||||
|
| `ZoneManager` | target-state only | Future zone entry/exit detection and LOD triggers. |
|
||||||
|
|
||||||
## GameManager is the orchestrator
|
## Target-State GameManager
|
||||||
|
|
||||||
|
`GameManager` does not exist in the current implementation. The following pattern is target-state guidance only and should not be applied until the manager exists in code.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export class GameManager {
|
export class GameManager {
|
||||||
@@ -51,7 +54,7 @@ export class GameManager {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Components and hooks access other managers **through GameManager only**:
|
When a `GameManager` exists, components and hooks should access other managers through it:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// Correct
|
// Correct
|
||||||
@@ -61,7 +64,7 @@ GameManager.getInstance().cinematic.play("intro");
|
|||||||
CinematicManager.getInstance().play("intro");
|
CinematicManager.getInstance().play("intro");
|
||||||
```
|
```
|
||||||
|
|
||||||
## Subscribe pattern (GameManager only)
|
## Target-State Subscribe Pattern
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
private listeners = new Set<() => void>()
|
private listeners = new Set<() => void>()
|
||||||
@@ -76,9 +79,9 @@ private emit(): void {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Every `set*()` method calls `this.emit()` to notify subscribers.
|
In that target-state manager, every `set*()` method calls `this.emit()` to notify subscribers.
|
||||||
|
|
||||||
## React bridge hook
|
## Target-State React Bridge Hook
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// hooks/useGameState.ts
|
// hooks/useGameState.ts
|
||||||
@@ -96,8 +99,8 @@ export function useGameState() {
|
|||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- Max 4 managers total
|
- Do not add a `GameManager` unless the feature requires a real shared gameplay state owner.
|
||||||
- Only `GameManager` holds durable state with `subscribe()`
|
- Current managers may be imported directly until the target-state orchestrator exists.
|
||||||
- Other managers are side-effect handlers — they do not store persistent state
|
- Keep singleton managers limited to side-effect services or shared interaction state.
|
||||||
- Always call `destroy()` on cleanup (App unmount)
|
- Always call `destroy()` on cleanup when a manager owns external resources.
|
||||||
- Never create manager instances with `new` — always use `.getInstance()`
|
- Never create manager instances with `new` — always use `.getInstance()`.
|
||||||
|
|||||||
@@ -66,21 +66,6 @@ import { RigidBody, CuboidCollider } from "@react-three/rapier";
|
|||||||
- `type="dynamic"` for movable objects
|
- `type="dynamic"` for movable objects
|
||||||
- Player uses `type="dynamic"` with `lockRotations`
|
- Player uses `type="dynamic"` with `lockRotations`
|
||||||
|
|
||||||
## Postprocessing
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing";
|
|
||||||
|
|
||||||
<EffectComposer>
|
|
||||||
<Bloom intensity={0.5} luminanceThreshold={0.9} />
|
|
||||||
<Vignette offset={0.3} darkness={0.5} />
|
|
||||||
</EffectComposer>;
|
|
||||||
```
|
|
||||||
|
|
||||||
- Always wrap in `<EffectComposer>`
|
|
||||||
- Keep effects minimal for performance
|
|
||||||
- Disable heavy effects on low-end devices via Debug panel
|
|
||||||
|
|
||||||
## What NOT to do
|
## What NOT to do
|
||||||
|
|
||||||
- Do not use `new THREE.Scene()` or `new THREE.WebGLRenderer()` — R3F handles this
|
- Do not use `new THREE.Scene()` or `new THREE.WebGLRenderer()` — R3F handles this
|
||||||
|
|||||||
+2
-1
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
*.glb filter=lfs diff=lfs merge=lfs -text
|
*.glb filter=lfs diff=lfs merge=lfs -text
|
||||||
*.gltf filter=lfs diff=lfs merge=lfs -text
|
*.gltf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.bin filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
# Textures
|
# Textures
|
||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -21,4 +22,4 @@
|
|||||||
|
|
||||||
# Video (cinematics)
|
# Video (cinematics)
|
||||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||||
*.webm filter=lfs diff=lfs merge=lfs -text
|
*.webm filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
@@ -74,12 +74,12 @@ jobs:
|
|||||||
|
|
||||||
- name: 📏 Check bundle size
|
- name: 📏 Check bundle size
|
||||||
run: |
|
run: |
|
||||||
# Get bundle size in KB
|
# Check generated app assets only; public/ model files are runtime assets copied to dist.
|
||||||
SIZE=$(du -k dist | cut -f1)
|
SIZE=$(du -k dist/assets | cut -f1)
|
||||||
echo "Bundle size: ${SIZE}KB"
|
echo "Bundle size: ${SIZE}KB"
|
||||||
|
|
||||||
# Threshold: 1000KB (configurable)
|
# Threshold: 5000KB (configurable)
|
||||||
THRESHOLD=1000
|
THRESHOLD=5000
|
||||||
|
|
||||||
if [ "$SIZE" -gt "$THRESHOLD" ]; then
|
if [ "$SIZE" -gt "$THRESHOLD" ]; then
|
||||||
echo "❌ Bundle size ${SIZE}KB exceeds threshold ${THRESHOLD}KB"
|
echo "❌ Bundle size ${SIZE}KB exceeds threshold ${THRESHOLD}KB"
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
name: 🔁 Weekly Branch Promotions
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 6 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: weekly-branch-promotions
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
open-promotion-pr:
|
||||||
|
name: Open ${{ matrix.head }} → ${{ matrix.base }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- head: develop
|
||||||
|
base: design
|
||||||
|
title: "chore: merge develop into design"
|
||||||
|
- head: design
|
||||||
|
base: main
|
||||||
|
title: "chore: merge design into main"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: ⬇️ Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: 🔁 Open promotion PR
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
BASE_BRANCH: ${{ matrix.base }}
|
||||||
|
HEAD_BRANCH: ${{ matrix.head }}
|
||||||
|
PR_TITLE: ${{ matrix.title }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
git fetch origin "$BASE_BRANCH" "$HEAD_BRANCH"
|
||||||
|
|
||||||
|
if git merge-base --is-ancestor "origin/$HEAD_BRANCH" "origin/$BASE_BRANCH"; then
|
||||||
|
echo "No promotion needed: $BASE_BRANCH already contains $HEAD_BRANCH."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
existing_pr="$(gh pr list \
|
||||||
|
--state open \
|
||||||
|
--base "$BASE_BRANCH" \
|
||||||
|
--head "$GITHUB_REPOSITORY_OWNER:$HEAD_BRANCH" \
|
||||||
|
--json number \
|
||||||
|
--jq '.[0].number // empty')"
|
||||||
|
|
||||||
|
if [ -n "$existing_pr" ]; then
|
||||||
|
echo "Promotion PR already open: #$existing_pr."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
gh pr create \
|
||||||
|
--base "$BASE_BRANCH" \
|
||||||
|
--head "$HEAD_BRANCH" \
|
||||||
|
--title "$PR_TITLE" \
|
||||||
|
--body "Automated weekly promotion PR from \`$HEAD_BRANCH\` to \`$BASE_BRANCH\`."
|
||||||
+6
-1
@@ -1,9 +1,14 @@
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.venv/
|
||||||
|
backend/.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
dist/
|
dist/
|
||||||
dist-ssr/
|
dist-ssr/
|
||||||
|
.vite/
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
@@ -37,4 +42,4 @@ Thumbs.db
|
|||||||
|
|
||||||
# 3D Assets Cache (drei, GLTFJSX)
|
# 3D Assets Cache (drei, GLTFJSX)
|
||||||
.drei/
|
.drei/
|
||||||
.glitchdrei-cache/
|
.glitchdrei-cache/
|
||||||
|
|||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
# Game Flow - La Fabrik
|
||||||
|
|
||||||
|
## Étapes du jeu
|
||||||
|
|
||||||
|
```
|
||||||
|
intro → start-intro → naming → bienvenue → star-move → mission2 → searching_problem → preparation → outOfFabrik
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Détail des étapes
|
||||||
|
|
||||||
|
### 1. `intro` (initial)
|
||||||
|
|
||||||
|
- État initial au chargement du jeu
|
||||||
|
- Aucune action, juste une étape de départ
|
||||||
|
- Transition automatique vers `start-intro`
|
||||||
|
|
||||||
|
### 2. `start-intro`
|
||||||
|
|
||||||
|
- **Déclenchement** : Auto-transition depuis `intro` quand la scène est chargée
|
||||||
|
- **Action** : Joue l'audio d'intro (`intro`)
|
||||||
|
- **Attente** : Attend que l'audio se termine
|
||||||
|
- **Transition** : Vers `naming` quand l'audio se termine
|
||||||
|
|
||||||
|
### 3. `naming`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand l'audio d'intro se termine
|
||||||
|
- **Action** : Affiche un input pour demander le prénom du joueur
|
||||||
|
- **Attente** : L'utilisateur entre son prénom et valide
|
||||||
|
- **Transition** : Vers `bienvenue` quand l'utilisateur valide
|
||||||
|
|
||||||
|
### 4. `bienvenue`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand l'utilisateur valide son prénom
|
||||||
|
- **Actions** :
|
||||||
|
- Affiche "Bienvenue {prénom} !" à l'écran
|
||||||
|
- Joue l'audio de bienvenue
|
||||||
|
- **Attente** : Attend que l'audio se termine
|
||||||
|
- **Transition** : Vers `star-move` quand l'audio se termine
|
||||||
|
|
||||||
|
### 5. `star-move`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand l'audio de bienvenue se termine
|
||||||
|
- **Action** : Active le mouvement du joueur (`setCanMove(true)`)
|
||||||
|
- **État** : Le joueur peut maintenant se déplacer librement
|
||||||
|
- **Zone** : La détection de zone devient active (ZoneDetection)
|
||||||
|
|
||||||
|
### 6. `mission2`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand le joueur entre dans la zone `fabrikExit` (position: `[-5, 25, -15]`)
|
||||||
|
- **Actions** :
|
||||||
|
- Stocke `activityCity: false` dans le store Zustand
|
||||||
|
- Joue l'audio `alertCentral`
|
||||||
|
- **État** : Les objets avec hook `useActivityCity()` détectent le changement et jouent leurs animations
|
||||||
|
- **Attente** : Le joueur atteint la zone de trigger pour `searching_problem`
|
||||||
|
|
||||||
|
### 7. `searching_problem`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand le joueur entre dans la zone `searchingProblemZone` (position: `[-5, 25, -30]`)
|
||||||
|
- **Actions** :
|
||||||
|
- Joue l'audio `searchingProblem`
|
||||||
|
- Affiche l'objet "central" (position: `[1, 15, -45]`)
|
||||||
|
- **Attente** : Le joueur interagit avec l'objet "central"
|
||||||
|
|
||||||
|
### 8. `preparation`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand le joueur interagit avec l'objet "central"
|
||||||
|
- **Actions** :
|
||||||
|
- Bloque le mouvement (`setCanMove(false)`)
|
||||||
|
- Cache l'objet "central"
|
||||||
|
|
||||||
|
### 9. `outOfFabrik`
|
||||||
|
|
||||||
|
- **Déclenchement** : (non implémenté pour le moment)
|
||||||
|
- **Action** : Transition vers l'étape finale
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fichiers clés
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
| --------------------------------------- | --------------------------------------------------------- |
|
||||||
|
| `src/stores/gameStore.ts` | Store Zustand pour l'état global du jeu |
|
||||||
|
| `src/stateManager/GameStepManager.ts` | Synchronise avec le store Zustand |
|
||||||
|
| `src/components/game/GameFlow.tsx` | Gère les transitions automatiques et la lecture audio |
|
||||||
|
| `src/components/ui/IntroUI.tsx` | Affiche l'input pour le prénom et le message de bienvenue |
|
||||||
|
| `src/components/zone/ZoneDetection.tsx` | Détecte quand le joueur entre dans une zone |
|
||||||
|
| `src/components/3d/CentralObject.tsx` | Objet interactif "central" pour la mission 2 |
|
||||||
|
| `src/data/audioConfig.ts` | Chemins des fichiers audio |
|
||||||
|
| `src/data/zones.ts` | Configuration des zones de transition |
|
||||||
|
| `src/hooks/useActivityCity.ts` | Hook pour détecter le changement d'activité de la ville |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration audio
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/data/audioConfig.ts
|
||||||
|
export const AUDIO_PATHS = {
|
||||||
|
intro: "/sounds/fa.mp3",
|
||||||
|
bienvenue: "/sounds/fa.mp3",
|
||||||
|
alertCentral: "/sounds/fa.mp3",
|
||||||
|
searchingProblem: "/sounds/fa.mp3",
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration des zones
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/data/zones.ts
|
||||||
|
export const ZONES: Zone[] = [
|
||||||
|
{
|
||||||
|
id: "fabrikExit",
|
||||||
|
position: [-5, 25, -15],
|
||||||
|
radius: 10,
|
||||||
|
height: 20,
|
||||||
|
targetStep: "mission2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "searchingProblemZone",
|
||||||
|
position: [-5, 25, -30],
|
||||||
|
radius: 10,
|
||||||
|
height: 20,
|
||||||
|
targetStep: "searching_problem",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Store Zustand
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/stores/gameStore.ts
|
||||||
|
interface GameState {
|
||||||
|
step: GameStep;
|
||||||
|
activityCity: boolean;
|
||||||
|
playerName: string;
|
||||||
|
canMove: boolean;
|
||||||
|
setStep: (step: GameStep) => void;
|
||||||
|
setActivityCity: (value: boolean) => void;
|
||||||
|
setPlayerName: (name: string) => void;
|
||||||
|
setCanMove: (canMove: boolean) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hooks personnalisés
|
||||||
|
|
||||||
|
### useActivityCity
|
||||||
|
|
||||||
|
Permet aux objets 3D de réagir au changement d'activité de la ville :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useActivityCity } from "@/hooks/useActivityCity";
|
||||||
|
|
||||||
|
function MyAnimatedObject() {
|
||||||
|
const activityCity = useActivityCity(); // true par défaut, false en mission2
|
||||||
|
|
||||||
|
// L'animation se déclenche quand activityCity change à false
|
||||||
|
// Utiliser useEffect pour réagir au changement
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug
|
||||||
|
|
||||||
|
En mode debug (`?debug` dans l'URL), on peut voir :
|
||||||
|
|
||||||
|
- **Game Step** : L'étape actuelle dans le panneau lil-gui
|
||||||
|
- **Player Position** : Position X, Y, Z du joueur en temps réel
|
||||||
|
- **Zone Visualization** : Anneaux visuels au sol pour les zones + cylindres transparents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes techniques
|
||||||
|
|
||||||
|
- Le mouvement du joueur est bloqué tant que `canMove` est `false`
|
||||||
|
- Le store Zustand (`useGameStore`) est la source principale de vérité
|
||||||
|
- `GameStepManager` synchronise automatiquement avec le store Zustand lors des transitions
|
||||||
|
- Les transitions via les zones utilisent `GameStepManager.transitionTo()` qui met à jour le store
|
||||||
|
- L'audio utilise un callback `onEnded` pour déclencher les transitions automatiques
|
||||||
@@ -1,137 +1,163 @@
|
|||||||
# La-Fabrik
|
# La-Fabrik
|
||||||
|
|
||||||
An interactive 3D web experience for La Fabrik Durable — a low-tech repair and transformation service in Altera, a post-capitalist city rebuilt in 2039. Players step into the role of a newly onboarded technician and experience a day at the service: repairing an e-bike, fixing a power grid, and upgrading a vertical farm's irrigation system.
|
La-Fabrik is an interactive 3D web experience built with React, Vite, Three.js, React Three Fiber, Rapier, GSAP, MediaPipe, and Zustand.
|
||||||
|
|
||||||
Built with React, Three.js, and Vite. Runs in the browser, no installation required.
|
The current prototype puts the player in a repair-oriented world where they progress through a short mission chain: intro, e-bike repair, power pylon repair, vertical farm repair, then outro. The project also includes a local editor for map, dialogue, subtitle, and cinematic data.
|
||||||
|
|
||||||
## 📦 Tech Stack
|
## Current Scope
|
||||||
|
|
||||||
### Build & Language
|
- Playable fullscreen 3D scene at `/`
|
||||||
|
- Production map loaded from `public/map.json`
|
||||||
|
- Progressive map/model/collision/stage loading overlay
|
||||||
|
- Player controller with pointer lock, `ZQSD` movement, jump, octree collision, trigger input, and grab input
|
||||||
|
- Reusable repair-game flow for `bike`, `pylone`, and `ferme`
|
||||||
|
- Repair case animation, exploded model scan, broken-part markers, grabbable replacements, snap-to-placeholder placement, install validation, reassembly, and completion
|
||||||
|
- Shared interaction system for trigger and grab objects
|
||||||
|
- Rapier physics for gameplay objects while the player keeps a Three.js octree collision controller
|
||||||
|
- Hand tracking through either a local Python WebSocket backend or browser-side MediaPipe
|
||||||
|
- Category-based audio manager for music, SFX, and dialogue
|
||||||
|
- Dialogue manifest, SRT subtitles, subtitle overlay, and dialogue queueing
|
||||||
|
- Cinematic manifest with GSAP camera keyframes and optional dialogue cues
|
||||||
|
- In-game settings menu for volumes, subtitles, subtitle language, and the currently staged repair-runtime toggle
|
||||||
|
- Debug mode with `?debug`, lil-gui controls, game-state panel, hand-tracking panel, debug camera, physics playground, and R3F perf
|
||||||
|
- `/editor` route for map transforms, SRT editing, dialogue manifest editing, cinematic manifest editing, preview, validation, export, and dev-server saves
|
||||||
|
- `/docs` route that renders the repository documentation inside the app
|
||||||
|
|
||||||
| Package |
|
## Routes
|
||||||
| -------------------------------------------------- |
|
|
||||||
| [TypeScript](https://www.typescriptlang.org/docs/) |
|
|
||||||
| [React](https://react.dev/learn) |
|
|
||||||
| [Vite](https://vite.dev/guide/) |
|
|
||||||
| [ESLint](https://eslint.org/docs/latest/) |
|
|
||||||
| [Prettier](https://prettier.io/docs/) |
|
|
||||||
|
|
||||||
### 3D Engine
|
| Route | Purpose |
|
||||||
|
| --------- | --------------------------------------------------- |
|
||||||
|
| `/` | Playable 3D experience |
|
||||||
|
| `/?debug` | Playable scene with debug GUI and overlays |
|
||||||
|
| `/editor` | Local map, dialogue, subtitle, and cinematic editor |
|
||||||
|
| `/docs` | In-app documentation index |
|
||||||
|
|
||||||
| Package |
|
## Tech Stack
|
||||||
| ----------------------------------------------------------------------------------------- |
|
|
||||||
| [Three.js](https://threejs.org/docs/) |
|
|
||||||
| [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) |
|
|
||||||
| [@react-three/drei](https://pmndrs.github.io/drei) |
|
|
||||||
| [@react-three/rapier](https://rapier.rs/docs/) |
|
|
||||||
| [@react-three/postprocessing](https://github.com/pmndrs/postprocessing) |
|
|
||||||
| [GSAP](https://gsap.com/docs/v3/Installation/) |
|
|
||||||
|
|
||||||
### Performance & Effects
|
| Area | Packages |
|
||||||
|
| --------------------- | ------------------------------------------------------------------------------ |
|
||||||
|
| App | React 19, TypeScript, Vite, TanStack Router |
|
||||||
|
| 3D | Three.js, React Three Fiber, drei |
|
||||||
|
| Physics and animation | `@react-three/rapier`, GSAP, Three.js `AnimationMixer` |
|
||||||
|
| State | Zustand, custom singleton managers where imperative runtime objects are needed |
|
||||||
|
| Hand tracking | `@mediapipe/tasks-vision`, optional FastAPI backend |
|
||||||
|
| Docs | `react-markdown`, `remark-gfm` |
|
||||||
|
| Quality | ESLint, Prettier, TypeScript project build |
|
||||||
|
|
||||||
| Package |
|
## Project Structure
|
||||||
| --------------------------------------------------------------------------- |
|
|
||||||
| [r3f-perf](https://github.com/utsuboco/r3f-perf) |
|
|
||||||
| [AnimationMixer](https://threejs.org/docs/#api/en/animation/AnimationMixer) |
|
|
||||||
|
|
||||||
## 🗂 Project Structure
|
```txt
|
||||||
|
.
|
||||||
```
|
|-- backend/ # Optional Python hand-tracking backend
|
||||||
la-fabrik/
|
|-- docs/
|
||||||
├── public/
|
| +-- technical/ # Architecture and implementation notes
|
||||||
│ ├── models/
|
| +-- user/ # Feature and user-facing guides
|
||||||
│ │ ├── map/ # Base map — loaded once at start
|
|-- public/
|
||||||
│ │ ├── workshop/
|
| +-- assets/ # UI videos, PDFs, logos, world videos
|
||||||
│ │ ├── powerGrid/
|
| +-- cinematics.json # Runtime cinematic manifest
|
||||||
│ │ └── farm/
|
| +-- map.json # Runtime/editor map data
|
||||||
│ ├── textures/
|
| +-- models/ # GLTF/GLB assets resolved by model folder name
|
||||||
│ └── sounds/
|
| +-- sounds/ # Music, SFX, dialogue audio, SRT subtitles
|
||||||
│
|
|-- src/
|
||||||
└── src/
|
| +-- components/
|
||||||
├── world/ # Single persistent 3D world
|
| | +-- docs/ # In-app docs layout and renderer
|
||||||
│ ├── World.tsx # Main scene composition
|
| | +-- editor/ # Editor panels and editor scene
|
||||||
│ ├── Map.tsx # Base map, always mounted
|
| | +-- three/ # R3F components by domain
|
||||||
│ ├── Lighting.tsx # Ambient, directional, point lights
|
| | +-- ui/ # HTML game/debug overlays
|
||||||
│ ├── Environment.tsx # HDRI, fog, sky
|
| +-- controls/ # Editor fly/player-style controls
|
||||||
│ ├── PostFX.tsx # Bloom, SSAO, chromatic aberration
|
| +-- data/ # Static tuning/config per domain
|
||||||
│ ├── zones/ # Spatial zones — LOD per zone
|
| +-- hooks/ # React hooks by domain
|
||||||
│ │ ├── WorkshopZone.tsx
|
| +-- lib/ # Browser hand-tracking helpers
|
||||||
│ │ ├── PowerGridZone.tsx
|
| +-- managers/ # Audio, interaction, and Zustand stores
|
||||||
│ │ ├── FarmZone.tsx
|
| +-- pages/ # Route-level pages
|
||||||
│ │ ├── SchoolZone.tsx
|
| +-- providers/ # Docs and hand-tracking providers
|
||||||
│ │ └── ResidentialZone.tsx
|
| +-- routes/ # Lazy route wrappers
|
||||||
│ └── player/
|
| +-- types/ # Shared domain types
|
||||||
│ ├── FPSController.tsx # PointerLockControls + Rapier movement
|
| +-- utils/ # Core, map, editor, dialogue, subtitle, Three helpers
|
||||||
│ └── Crosshair.tsx
|
| +-- world/ # Production/debug world composition and player
|
||||||
│
|
`-- vite.config.ts # Vite config plus local editor save endpoints
|
||||||
├── components/
|
|
||||||
│ ├── 3d/ # Shared reusable 3D elements
|
|
||||||
│ │ └── InteractiveObject.tsx # Raycasting + outline wrapper
|
|
||||||
│ └── ui/ # HTML overlays — outside Canvas
|
|
||||||
│ ├── NarrativeOverlay.tsx # Floating dialogues
|
|
||||||
│ ├── MissionHUD.tsx # Current objective
|
|
||||||
│ ├── MapHUD.tsx # Minimap
|
|
||||||
│ ├── CinematicBars.tsx # GSAP black bars
|
|
||||||
│ └── LoadingScreen.tsx # Asset progress
|
|
||||||
│
|
|
||||||
├── stateManager/ # All logic, state, orchestration
|
|
||||||
│ ├── GameManager.ts # Single source of truth: phase, zone, mission
|
|
||||||
│ ├── CinematicManager.ts # GSAP timelines, camera lock/unlock
|
|
||||||
│ ├── AudioManager.ts # Music, SFX, spatial audio
|
|
||||||
│ └── ZoneManager.ts # Zone detection, LOD triggers
|
|
||||||
│
|
|
||||||
├── hooks/ # React hooks — thin wrappers on managers
|
|
||||||
│ ├── useGameState.ts # Subscribes to GameManager
|
|
||||||
│ ├── useZoneDetection.ts
|
|
||||||
│ ├── useInteraction.ts
|
|
||||||
│ ├── useCinematic.ts
|
|
||||||
│ ├── useAudio.ts
|
|
||||||
│ └── useLOD.ts
|
|
||||||
│
|
|
||||||
├── data/
|
|
||||||
│ ├── zones.ts # { id, position, radius, missionId }
|
|
||||||
│ ├── dialogues.ts # Narrative scripts, PNJ states
|
|
||||||
│ └── missions.ts # Mission definitions, steps
|
|
||||||
│
|
|
||||||
├── shaders/
|
|
||||||
│ └── hologram/
|
|
||||||
│ ├── vertex.glsl
|
|
||||||
│ └── fragment.glsl
|
|
||||||
│
|
|
||||||
├── utils/
|
|
||||||
│ ├── EventEmitter.ts # Simple typed pub/sub utility
|
|
||||||
│ ├── Sizes.ts # Viewport size tracking
|
|
||||||
│ ├── Time.ts # Animation frame timing utility
|
|
||||||
│ └── debug/ # Dev-only tools and scene inspection
|
|
||||||
│ ├── Debug.ts # Global lil-gui manager
|
|
||||||
│ ├── DebugPerf.tsx # r3f-perf overlay mounted in Canvas
|
|
||||||
│ ├── isDebugEnabled.ts # Debug query-string helper
|
|
||||||
│ └── scene/
|
|
||||||
│ ├── DebugHelpers.tsx # Grid + axes helpers shown in debug mode
|
|
||||||
│ └── DebugCameraControls.tsx # Free debug camera for map inspection
|
|
||||||
├── hooks/
|
|
||||||
│ └── debug/
|
|
||||||
│ ├── useCameraMode.ts
|
|
||||||
│ ├── useDebugFolder.ts
|
|
||||||
│ ├── useDebugStore.ts
|
|
||||||
│ └── useSceneMode.ts
|
|
||||||
│
|
|
||||||
├── App.tsx # Canvas bootstrap
|
|
||||||
└── main.tsx
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
Install and run the frontend:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/La-Fabrik-Durable/La-Fabrik.git
|
|
||||||
cd La-Fabrik
|
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
- app: `http://localhost:5173`
|
Open:
|
||||||
- debug mode: `http://localhost:5173?debug`
|
|
||||||
|
|
||||||
## 📜 License
|
```txt
|
||||||
|
http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
See [LICENSE](./LICENSE) file.
|
Useful local URLs:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
http://localhost:5173/?debug
|
||||||
|
http://localhost:5173/editor
|
||||||
|
http://localhost:5173/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
Run checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck
|
||||||
|
npm run lint
|
||||||
|
npm run format:check
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional Hand-Tracking Backend
|
||||||
|
|
||||||
|
The app can use browser-side MediaPipe, but the default debug source is the local backend.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3.11 -m venv backend/.venv
|
||||||
|
source backend/.venv/bin/activate
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install -r backend/requirements.txt
|
||||||
|
python backend/download_model.py
|
||||||
|
python -m backend.main
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend endpoints:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
GET http://localhost:8000/health
|
||||||
|
WS ws://localhost:8000/ws
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation Index
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| --------------------------------------- | ---------------------------------------------------------- |
|
||||||
|
| `docs/technical/architecture.md` | Current runtime architecture |
|
||||||
|
| `docs/technical/scene-runtime.md` | Scene loading, world composition, and player spawn gates |
|
||||||
|
| `docs/technical/repair-game.md` | Repair-game implementation and state flow |
|
||||||
|
| `docs/technical/interaction.md` | Trigger, grab, focus, and hand-grab system |
|
||||||
|
| `docs/technical/target-architecture.md` | Intended medium-term architecture direction |
|
||||||
|
| `docs/technical/audio.md` | Music, SFX, dialogue, subtitles, and editor validation |
|
||||||
|
| `docs/technical/hand-tracking.md` | Webcam, backend/browser MediaPipe, glove, and gesture flow |
|
||||||
|
| `docs/technical/zustand.md` | Game, settings, and subtitle stores |
|
||||||
|
| `docs/technical/three-debugging.md` | DevTools workflow for stepping into Three.js internals |
|
||||||
|
| `docs/technical/editor.md` | Editor implementation details |
|
||||||
|
| `docs/technical/animation.md` | Animated, explodable, and reusable 3D model components |
|
||||||
|
| `docs/user/features.md` | Implemented feature inventory |
|
||||||
|
| `docs/user/main-feature.md` | User-facing repair-game walkthrough |
|
||||||
|
| `docs/user/editor.md` | Editor user guide |
|
||||||
|
| `docs/code-review-preparation.md` | French code-review preparation support |
|
||||||
|
|
||||||
|
## Current Caveats
|
||||||
|
|
||||||
|
- This is still a prototype, not a complete game runtime.
|
||||||
|
- The repair-runtime toggle is stored in settings and displayed in the UI, but the repair game currently still runs locally in React/Three.
|
||||||
|
- `useRepairMovementLocked()` currently returns `false`, so the movement-lock rule and indicator are present but disabled on `develop`.
|
||||||
|
- Production editor persistence does not exist. Save endpoints in `vite.config.ts` are local Vite dev-server helpers.
|
||||||
|
- The player uses octree collision while gameplay objects use Rapier. Keep that boundary deliberate unless the whole player controller is migrated.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See `LICENSE`.
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Hand Tracking Backend
|
||||||
|
|
||||||
|
Remote-compatible Python backend for La-Fabrik hand tracking.
|
||||||
|
|
||||||
|
The browser captures webcam frames, downsizes them, sends JPEG frames to this backend over WebSocket, and receives hand landmarks plus closed-fist state.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3.11 -m venv backend/.venv
|
||||||
|
source backend/.venv/bin/activate
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install -r backend/requirements.txt
|
||||||
|
python backend/download_model.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
Run the Vite frontend and the Python backend in two separate terminals.
|
||||||
|
|
||||||
|
Terminal 1:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminal 2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source backend/.venv/bin/activate
|
||||||
|
python -m backend.main
|
||||||
|
```
|
||||||
|
|
||||||
|
The WebSocket endpoint is:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ws://localhost:8000/ws
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Check
|
||||||
|
|
||||||
|
```txt
|
||||||
|
http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Message Flow
|
||||||
|
|
||||||
|
Client sends a compressed frame:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"timestamp": 1234567890,
|
||||||
|
"width": 320,
|
||||||
|
"height": 240,
|
||||||
|
"image": "base64-jpeg"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Server responds with detected hands:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "hands",
|
||||||
|
"timestamp": 1234567890,
|
||||||
|
"hands": [
|
||||||
|
{
|
||||||
|
"x": 0.5,
|
||||||
|
"y": 0.3,
|
||||||
|
"z": 0.1,
|
||||||
|
"landmarks": [
|
||||||
|
{
|
||||||
|
"x": 0.48,
|
||||||
|
"y": 0.32,
|
||||||
|
"z": 0.02
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handedness": "Right",
|
||||||
|
"isFist": true,
|
||||||
|
"score": 0.92
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The backend does not read `cv2.VideoCapture(0)`.
|
||||||
|
- This keeps local development and production behavior aligned.
|
||||||
|
- Each browser connection sends its own webcam frames.
|
||||||
|
- The backend rate-limits frames per connection and drops work when a client is already being processed.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from fastapi import WebSocket
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ClientConnection:
|
||||||
|
id: str
|
||||||
|
websocket: WebSocket
|
||||||
|
is_processing: bool = False
|
||||||
|
last_frame_at: float = 0.0
|
||||||
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionManager:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._connections: dict[str, ClientConnection] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count(self) -> int:
|
||||||
|
return len(self._connections)
|
||||||
|
|
||||||
|
async def connect(self, websocket: WebSocket) -> ClientConnection:
|
||||||
|
await websocket.accept()
|
||||||
|
connection = ClientConnection(id=str(uuid4()), websocket=websocket)
|
||||||
|
self._connections[connection.id] = connection
|
||||||
|
return connection
|
||||||
|
|
||||||
|
def disconnect(self, connection: ClientConnection) -> None:
|
||||||
|
self._connections.pop(connection.id, None)
|
||||||
|
|
||||||
|
async def send(self, connection: ClientConnection, payload: dict[str, Any]) -> None:
|
||||||
|
await connection.websocket.send_json(payload)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.request import urlretrieve
|
||||||
|
|
||||||
|
|
||||||
|
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task"
|
||||||
|
MODEL_PATH = Path(__file__).with_name("hand_landmarker.task")
|
||||||
|
|
||||||
|
|
||||||
|
def download_model() -> None:
|
||||||
|
if MODEL_PATH.exists():
|
||||||
|
print(f"Model already exists at {MODEL_PATH}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Downloading MediaPipe Hand Landmarker model...")
|
||||||
|
urlretrieve(MODEL_URL, MODEL_PATH)
|
||||||
|
print(f"Model downloaded to {MODEL_PATH}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
download_model()
|
||||||
Binary file not shown.
@@ -0,0 +1,142 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import mediapipe as mp
|
||||||
|
import numpy as np
|
||||||
|
from mediapipe.tasks import python
|
||||||
|
from mediapipe.tasks.python import vision
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class HandData:
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
z: float
|
||||||
|
landmarks: list[dict[str, float]]
|
||||||
|
handedness: str
|
||||||
|
is_fist: bool
|
||||||
|
score: float
|
||||||
|
|
||||||
|
def to_payload(self) -> dict[str, float | str | bool | list[dict[str, float]]]:
|
||||||
|
return {
|
||||||
|
"x": self.x,
|
||||||
|
"y": self.y,
|
||||||
|
"z": self.z,
|
||||||
|
"landmarks": self.landmarks,
|
||||||
|
"handedness": self.handedness,
|
||||||
|
"isFist": self.is_fist,
|
||||||
|
"score": self.score,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HandTracker:
|
||||||
|
def __init__(self, max_hands: int = 2) -> None:
|
||||||
|
model_path = Path(__file__).with_name("hand_landmarker.task")
|
||||||
|
if not model_path.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"Missing hand_landmarker.task. Run `python backend/download_model.py`.",
|
||||||
|
)
|
||||||
|
|
||||||
|
base_options = python.BaseOptions(model_asset_path=str(model_path))
|
||||||
|
options = vision.HandLandmarkerOptions(
|
||||||
|
base_options=base_options,
|
||||||
|
running_mode=vision.RunningMode.IMAGE,
|
||||||
|
num_hands=max_hands,
|
||||||
|
)
|
||||||
|
self._detector = vision.HandLandmarker.create_from_options(options)
|
||||||
|
|
||||||
|
def detect_from_base64_jpeg(self, image_base64: str) -> list[HandData]:
|
||||||
|
image_data = base64.b64decode(image_base64, validate=True)
|
||||||
|
image_buffer = np.frombuffer(image_data, dtype=np.uint8)
|
||||||
|
frame = cv2.imdecode(image_buffer, cv2.IMREAD_COLOR)
|
||||||
|
if frame is None:
|
||||||
|
raise ValueError("Invalid JPEG frame")
|
||||||
|
|
||||||
|
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||||
|
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_frame)
|
||||||
|
result = self._detector.detect(mp_image)
|
||||||
|
return self._to_hands(result)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self._detector.close()
|
||||||
|
|
||||||
|
def _to_hands(self, result: vision.HandLandmarkerResult) -> list[HandData]:
|
||||||
|
hands: list[HandData] = []
|
||||||
|
if not result.hand_landmarks or not result.handedness:
|
||||||
|
return hands
|
||||||
|
|
||||||
|
for landmarks, handedness_categories in zip(
|
||||||
|
result.hand_landmarks,
|
||||||
|
result.handedness,
|
||||||
|
):
|
||||||
|
palm_center = self._average_points(
|
||||||
|
[landmarks[0], landmarks[5], landmarks[9], landmarks[13], landmarks[17]],
|
||||||
|
)
|
||||||
|
is_fist = self._is_fist(landmarks)
|
||||||
|
handedness = handedness_categories[0]
|
||||||
|
|
||||||
|
hands.append(
|
||||||
|
HandData(
|
||||||
|
x=palm_center["x"],
|
||||||
|
y=palm_center["y"],
|
||||||
|
z=palm_center["z"],
|
||||||
|
landmarks=[
|
||||||
|
{"x": point.x, "y": point.y, "z": point.z}
|
||||||
|
for point in landmarks
|
||||||
|
],
|
||||||
|
handedness=handedness.category_name,
|
||||||
|
is_fist=is_fist,
|
||||||
|
score=handedness.score,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return hands
|
||||||
|
|
||||||
|
def _is_fist(self, landmarks: list[Any]) -> bool:
|
||||||
|
palm_center = self._average_points(
|
||||||
|
[landmarks[0], landmarks[5], landmarks[9], landmarks[13], landmarks[17]],
|
||||||
|
)
|
||||||
|
palm_size = self._calculate_distance(landmarks[0], landmarks[9])
|
||||||
|
if palm_size <= 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
folded_finger_count = sum(
|
||||||
|
self._calculate_distance(landmarks[index], palm_center) / palm_size < 1.05
|
||||||
|
for index in (8, 12, 16, 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
return folded_finger_count >= 4
|
||||||
|
|
||||||
|
def _average_points(self, points: list[Any]) -> dict[str, float]:
|
||||||
|
return {
|
||||||
|
"x": sum(point.x for point in points) / len(points),
|
||||||
|
"y": sum(point.y for point in points) / len(points),
|
||||||
|
"z": sum(point.z for point in points) / len(points),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _calculate_distance(self, point_a: Any, point_b: Any) -> float:
|
||||||
|
return math.sqrt(
|
||||||
|
(self._get_coordinate(point_a, "x") - self._get_coordinate(point_b, "x"))
|
||||||
|
** 2
|
||||||
|
+ (self._get_coordinate(point_a, "y") - self._get_coordinate(point_b, "y"))
|
||||||
|
** 2
|
||||||
|
+ (self._get_coordinate(point_a, "z") - self._get_coordinate(point_b, "z"))
|
||||||
|
** 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_coordinate(self, point: Any, axis: str) -> float:
|
||||||
|
if isinstance(point, dict):
|
||||||
|
return point[axis]
|
||||||
|
|
||||||
|
return getattr(point, axis)
|
||||||
|
|
||||||
|
|
||||||
|
def now_ms() -> int:
|
||||||
|
return time.monotonic_ns() // 1_000_000
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from backend.connection_manager import ClientConnection, ConnectionManager
|
||||||
|
from backend.hand_tracker import HandTracker, now_ms
|
||||||
|
|
||||||
|
|
||||||
|
MAX_FRAME_BYTES = 220_000
|
||||||
|
MIN_FRAME_INTERVAL_SECONDS = 0.08
|
||||||
|
|
||||||
|
manager = ConnectionManager()
|
||||||
|
tracker: HandTracker | None = None
|
||||||
|
detection_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
global tracker
|
||||||
|
tracker = HandTracker(max_hands=2)
|
||||||
|
yield
|
||||||
|
if tracker:
|
||||||
|
tracker.close()
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="La-Fabrik Hand Tracking", lifespan=lifespan)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health() -> JSONResponse:
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"connections": manager.count,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket) -> None:
|
||||||
|
connection = await manager.connect(websocket)
|
||||||
|
await manager.send(connection, status_payload("connected"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
message = await websocket.receive_json()
|
||||||
|
response = await handle_message(connection, message)
|
||||||
|
await manager.send(connection, response)
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
manager.disconnect(connection)
|
||||||
|
except Exception as error:
|
||||||
|
await manager.send(connection, error_payload(str(error)))
|
||||||
|
manager.disconnect(connection)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_message(
|
||||||
|
connection: ClientConnection,
|
||||||
|
message: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if message.get("type") != "frame":
|
||||||
|
return error_payload("Unsupported message type")
|
||||||
|
|
||||||
|
current_time = asyncio.get_running_loop().time()
|
||||||
|
if current_time - connection.last_frame_at < MIN_FRAME_INTERVAL_SECONDS:
|
||||||
|
return status_payload("rate_limited")
|
||||||
|
|
||||||
|
if connection.is_processing:
|
||||||
|
return status_payload("busy")
|
||||||
|
|
||||||
|
image = message.get("image")
|
||||||
|
if not isinstance(image, str):
|
||||||
|
return error_payload("Missing image payload")
|
||||||
|
|
||||||
|
if len(image) > MAX_FRAME_BYTES:
|
||||||
|
return error_payload("Frame payload too large")
|
||||||
|
|
||||||
|
if tracker is None:
|
||||||
|
return error_payload("Hand tracker is not ready")
|
||||||
|
|
||||||
|
if detection_lock.locked():
|
||||||
|
return status_payload("busy")
|
||||||
|
|
||||||
|
connection.last_frame_at = current_time
|
||||||
|
connection.is_processing = True
|
||||||
|
try:
|
||||||
|
async with detection_lock:
|
||||||
|
hands = await asyncio.to_thread(tracker.detect_from_base64_jpeg, image)
|
||||||
|
return {
|
||||||
|
"type": "hands",
|
||||||
|
"timestamp": now_ms(),
|
||||||
|
"hands": [hand.to_payload() for hand in hands],
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
connection.is_processing = False
|
||||||
|
|
||||||
|
|
||||||
|
def status_payload(status: str) -> dict[str, str | int]:
|
||||||
|
return {
|
||||||
|
"type": "status",
|
||||||
|
"timestamp": now_ms(),
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def error_payload(message: str) -> dict[str, str | int | list[Any]]:
|
||||||
|
return {
|
||||||
|
"type": "error",
|
||||||
|
"timestamp": now_ms(),
|
||||||
|
"hands": [],
|
||||||
|
"message": message,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi==0.115.0
|
||||||
|
uvicorn[standard]==0.30.6
|
||||||
|
opencv-python-headless==4.10.0.84
|
||||||
|
mediapipe==0.10.20
|
||||||
|
numpy==1.26.4
|
||||||
@@ -0,0 +1,930 @@
|
|||||||
|
# Préparation Code Review - La-Fabrik
|
||||||
|
|
||||||
|
Ce document est une fiche de révision pour préparer la code review.
|
||||||
|
|
||||||
|
Il est basé sur `develop`, après le merge récent autour de l'environnement, du repair game, de l'audio, du store Zustand, du hand tracking, de l'éditeur et de la doc intégrée.
|
||||||
|
|
||||||
|
Le but n'est pas de réciter le repo. Le but est de savoir :
|
||||||
|
|
||||||
|
- expliquer l'architecture générale ;
|
||||||
|
- naviguer vite entre les bons fichiers ;
|
||||||
|
- défendre les choix techniques ;
|
||||||
|
- montrer une démo propre avec Chrome DevTools ;
|
||||||
|
- reconnaître honnêtement les limites actuelles.
|
||||||
|
|
||||||
|
## Ce qu'il faut retenir en premier
|
||||||
|
|
||||||
|
La-Fabrik est une expérience web 3D en React, Vite, Three.js et React Three Fiber.
|
||||||
|
|
||||||
|
Le joueur est dans un monde 3D et avance dans une progression de réparation :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
intro -> bike -> pylone -> ferme -> outro
|
||||||
|
```
|
||||||
|
|
||||||
|
Les trois piliers à connaître pour la review :
|
||||||
|
|
||||||
|
1. `RepairGame` : boucle de gameplay principale.
|
||||||
|
2. `AudioManager` : musique, SFX, dialogues et sous-titres.
|
||||||
|
3. `useGameStore` / Zustand : progression globale du jeu.
|
||||||
|
|
||||||
|
Le reste du repo sert à intégrer ces piliers :
|
||||||
|
|
||||||
|
- `World` monte la scène 3D ;
|
||||||
|
- `InteractionManager` relie les objets interactifs aux inputs ;
|
||||||
|
- `HandTrackingProvider` active la webcam seulement quand elle est utile ;
|
||||||
|
- `/editor` permet de modifier map, dialogues, SRT et cinématiques ;
|
||||||
|
- `/docs` rend la documentation Markdown dans l'app.
|
||||||
|
|
||||||
|
## Pitch en 30 secondes
|
||||||
|
|
||||||
|
Phrase simple à savoir dire :
|
||||||
|
|
||||||
|
> La-Fabrik est une expérience 3D interactive en React Three Fiber. Le joueur progresse dans des missions de réparation. Le coeur du gameplay est un `RepairGame` réutilisable pour le vélo, le pylône et la ferme. La progression est centralisée dans Zustand, les interactions passent par un manager commun, les objets manipulables utilisent Rapier, et le joueur garde une collision octree séparée. L'audio est géré par un `AudioManager` avec volumes par catégorie, dialogues et sous-titres.
|
||||||
|
|
||||||
|
## Carte mentale du projet
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/main.tsx
|
||||||
|
-> App.tsx
|
||||||
|
-> router.tsx
|
||||||
|
-> / HomePage + Canvas + World + GameUI
|
||||||
|
-> /editor EditorPage
|
||||||
|
-> /docs DocsLayout + Markdown pages
|
||||||
|
```
|
||||||
|
|
||||||
|
Dans la scène jouable :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
HomePage
|
||||||
|
-> HandTrackingProvider
|
||||||
|
-> Canvas
|
||||||
|
-> World
|
||||||
|
-> GameMap
|
||||||
|
-> GameStageContent
|
||||||
|
-> RepairGame bike/pylone/ferme
|
||||||
|
-> GameMusic
|
||||||
|
-> GameDialogues
|
||||||
|
-> Player
|
||||||
|
-> GameUI
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features à connaître
|
||||||
|
|
||||||
|
### Runtime 3D
|
||||||
|
|
||||||
|
Fichiers :
|
||||||
|
|
||||||
|
- `src/pages/page.tsx`
|
||||||
|
- `src/world/World.tsx`
|
||||||
|
- `src/hooks/world/useWorldSceneLoading.ts`
|
||||||
|
- `src/world/GameMap.tsx`
|
||||||
|
- `src/world/GameMapCollision.tsx`
|
||||||
|
|
||||||
|
Ce que ça fait :
|
||||||
|
|
||||||
|
- charge `public/map.json` ;
|
||||||
|
- résout les modèles dans `public/models/{name}/model.glb` ou `model.gltf` ;
|
||||||
|
- affiche des cubes fallback si un modèle manque ;
|
||||||
|
- construit l'octree joueur depuis les nodes de collision ;
|
||||||
|
- attend que map, collision, stage et octree soient prêts avant de spawn le joueur.
|
||||||
|
|
||||||
|
Phrase à retenir :
|
||||||
|
|
||||||
|
> `World` ne lance pas tout d'un coup. Il attend que la map, l'octree et le stage gameplay soient prêts avant de monter le joueur, la musique et les dialogues.
|
||||||
|
|
||||||
|
Piège à connaître :
|
||||||
|
|
||||||
|
Les anciens flags comme `noMusic`, `noMap`, `noDialogues`, `noPlayer` ne sont plus branchés dans `World`. Pour la démo, utiliser surtout `?debug`.
|
||||||
|
|
||||||
|
### Player et collision
|
||||||
|
|
||||||
|
Fichiers :
|
||||||
|
|
||||||
|
- `src/world/player/PlayerController.tsx`
|
||||||
|
- `src/data/input/keybindings.ts`
|
||||||
|
- `src/data/player/playerConfig.ts`
|
||||||
|
- `src/world/GameMapCollision.tsx`
|
||||||
|
|
||||||
|
Ce que ça fait :
|
||||||
|
|
||||||
|
- déplacement `ZQSD` ;
|
||||||
|
- souris en pointer lock ;
|
||||||
|
- saut avec `Space` ;
|
||||||
|
- interaction avec `E` ;
|
||||||
|
- grab avec clic gauche ;
|
||||||
|
- collision joueur via capsule Three.js + octree.
|
||||||
|
|
||||||
|
Phrase à retenir :
|
||||||
|
|
||||||
|
> Le joueur n'est pas piloté par Rapier. Rapier sert aux objets manipulables, alors que le joueur utilise une capsule et un octree.
|
||||||
|
|
||||||
|
Piège à connaître :
|
||||||
|
|
||||||
|
`useRepairMovementLocked()` retourne actuellement `false`. Le lock de mouvement est prévu dans le code et l'UI, mais il est désactivé sur `develop`.
|
||||||
|
|
||||||
|
### Interaction
|
||||||
|
|
||||||
|
Fichiers :
|
||||||
|
|
||||||
|
- `src/managers/InteractionManager.ts`
|
||||||
|
- `src/hooks/interaction/useInteraction.ts`
|
||||||
|
- `src/components/three/interaction/InteractableObject.tsx`
|
||||||
|
- `src/components/three/interaction/TriggerObject.tsx`
|
||||||
|
- `src/components/three/interaction/GrabbableObject.tsx`
|
||||||
|
- `src/components/ui/InteractPrompt.tsx`
|
||||||
|
|
||||||
|
Ce que ça fait :
|
||||||
|
|
||||||
|
- détecte si un objet est proche ;
|
||||||
|
- raycast depuis la caméra pour savoir si le joueur vise l'objet ;
|
||||||
|
- affiche le prompt `E` pour les triggers ;
|
||||||
|
- gère les grabs souris et hand tracking ;
|
||||||
|
- expose un snapshot React via `useSyncExternalStore`.
|
||||||
|
|
||||||
|
Phrase à retenir :
|
||||||
|
|
||||||
|
> Les interactions sont séparées du gameplay. Un objet dit juste "je suis trigger" ou "je suis grabbable", puis le player controller déclenche l'action selon le focus courant.
|
||||||
|
|
||||||
|
### Repair game
|
||||||
|
|
||||||
|
Fichiers coeur :
|
||||||
|
|
||||||
|
- `src/components/three/gameplay/RepairGame.tsx`
|
||||||
|
- `src/components/three/gameplay/RepairRepairingStep.tsx`
|
||||||
|
- `src/components/three/gameplay/RepairScanSequence.tsx`
|
||||||
|
- `src/components/three/gameplay/RepairMissionCase.tsx`
|
||||||
|
- `src/components/three/gameplay/RepairCaseModel.tsx`
|
||||||
|
- `src/components/three/gameplay/RepairCompletionStep.tsx`
|
||||||
|
|
||||||
|
Fichiers data/state :
|
||||||
|
|
||||||
|
- `src/data/gameplay/repairMissions.ts`
|
||||||
|
- `src/data/gameplay/repairCaseConfig.ts`
|
||||||
|
- `src/data/gameplay/repairGameConfig.ts`
|
||||||
|
- `src/types/gameplay/repairMission.ts`
|
||||||
|
- `src/managers/stores/useGameStore.ts`
|
||||||
|
|
||||||
|
Flow :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
locked
|
||||||
|
-> waiting
|
||||||
|
-> inspected
|
||||||
|
-> fragmented
|
||||||
|
-> scanning
|
||||||
|
-> repairing
|
||||||
|
-> reassembling
|
||||||
|
-> done
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce que ça fait :
|
||||||
|
|
||||||
|
- inspecter l'objet de mission ;
|
||||||
|
- afficher une mallette ;
|
||||||
|
- ouvrir/interagir avec la mallette ;
|
||||||
|
- fragmenter le modèle ;
|
||||||
|
- scanner les pièces cassées ;
|
||||||
|
- proposer plusieurs pièces de remplacement ;
|
||||||
|
- attraper et snapper les pièces ;
|
||||||
|
- vérifier bonne pièce + pièces cassées rangées ;
|
||||||
|
- réassembler le modèle ;
|
||||||
|
- valider la mission et passer à la suivante.
|
||||||
|
|
||||||
|
Phrase à retenir :
|
||||||
|
|
||||||
|
> `RepairGame` est un orchestrateur de steps. Les variations entre vélo, pylône et ferme sont dans `repairMissions.ts`, pas hardcodées dans trois composants séparés.
|
||||||
|
|
||||||
|
### Audio, dialogues, sous-titres
|
||||||
|
|
||||||
|
Fichiers :
|
||||||
|
|
||||||
|
- `src/managers/AudioManager.ts`
|
||||||
|
- `src/world/GameMusic.tsx`
|
||||||
|
- `src/utils/dialogues/playDialogue.ts`
|
||||||
|
- `src/utils/dialogues/loadDialogueManifest.ts`
|
||||||
|
- `src/managers/stores/useSettingsStore.ts`
|
||||||
|
- `src/managers/stores/useSubtitleStore.ts`
|
||||||
|
- `src/components/ui/Subtitles.tsx`
|
||||||
|
|
||||||
|
Ce que ça fait :
|
||||||
|
|
||||||
|
- musique en loop ;
|
||||||
|
- sons one-shot avec pooling ;
|
||||||
|
- volumes par catégorie : `music`, `sfx`, `dialogue` ;
|
||||||
|
- dialogues depuis `public/sounds/dialogue/dialogues.json` ;
|
||||||
|
- SRT par voix et par langue ;
|
||||||
|
- sous-titres synchronisés avec `audio.currentTime` ;
|
||||||
|
- queue de dialogues pour éviter les overlaps.
|
||||||
|
|
||||||
|
Phrase à retenir :
|
||||||
|
|
||||||
|
> L'audio est impératif, donc il est dans un manager. React garde les réglages et les sous-titres, mais les vrais `HTMLAudioElement` sont gérés par `AudioManager`.
|
||||||
|
|
||||||
|
### Zustand
|
||||||
|
|
||||||
|
Fichiers :
|
||||||
|
|
||||||
|
- `src/managers/stores/useGameStore.ts`
|
||||||
|
- `src/managers/stores/useSettingsStore.ts`
|
||||||
|
- `src/managers/stores/useSubtitleStore.ts`
|
||||||
|
- `src/components/ui/debug/GameStateDebugPanel.tsx`
|
||||||
|
|
||||||
|
Ce que ça fait :
|
||||||
|
|
||||||
|
- `useGameStore` : progression globale ;
|
||||||
|
- `useSettingsStore` : menu, volumes, sous-titres, runtime repair ;
|
||||||
|
- `useSubtitleStore` : sous-titre actif ;
|
||||||
|
- debug panel : manipule le même store que le vrai gameplay.
|
||||||
|
|
||||||
|
Phrase à retenir :
|
||||||
|
|
||||||
|
> Zustand contient l'état durable. Les valeurs qui changent à chaque frame restent dans des refs, dans R3F ou dans les managers.
|
||||||
|
|
||||||
|
### Hand tracking
|
||||||
|
|
||||||
|
Fichiers :
|
||||||
|
|
||||||
|
- `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`
|
||||||
|
|
||||||
|
Ce que ça fait :
|
||||||
|
|
||||||
|
- source `backend` avec Python/FastAPI/MediaPipe ;
|
||||||
|
- source `browser` avec `@mediapipe/tasks-vision` ;
|
||||||
|
- activation seulement quand utile ;
|
||||||
|
- gesture deux poings fermés ;
|
||||||
|
- grab à la main pour certains objets ;
|
||||||
|
- visualisation gants et panel debug.
|
||||||
|
|
||||||
|
Phrase à retenir :
|
||||||
|
|
||||||
|
> Le hand tracking n'est pas actif tout le temps. Le provider l'active quand le contexte le justifie, par exemple pendant certaines étapes du repair game.
|
||||||
|
|
||||||
|
### Editor
|
||||||
|
|
||||||
|
Fichiers :
|
||||||
|
|
||||||
|
- `src/pages/editor/page.tsx`
|
||||||
|
- `src/components/editor/EditorControls.tsx`
|
||||||
|
- `src/components/editor/scene/EditorScene.tsx`
|
||||||
|
- `src/components/editor/scene/EditorMap.tsx`
|
||||||
|
- `vite.config.ts`
|
||||||
|
|
||||||
|
Ce que ça fait :
|
||||||
|
|
||||||
|
- édite `public/map.json` ;
|
||||||
|
- affiche et transforme les objets ;
|
||||||
|
- édite dialogues, SRT et cinématiques ;
|
||||||
|
- preview audio/cinématique ;
|
||||||
|
- sauvegarde locale via endpoints Vite.
|
||||||
|
|
||||||
|
Phrase à retenir :
|
||||||
|
|
||||||
|
> L'éditeur partage les mêmes formats que le runtime. Il n'y a pas un format map pour l'éditeur et un autre pour le jeu.
|
||||||
|
|
||||||
|
Piège à connaître :
|
||||||
|
|
||||||
|
Les endpoints `/api/save-*` sont des helpers Vite en dev local, pas une API de production.
|
||||||
|
|
||||||
|
## Bloc principal 1 : RepairGame
|
||||||
|
|
||||||
|
### Ce qu'il faut comprendre
|
||||||
|
|
||||||
|
`RepairGame` ne fait pas "juste afficher une réparation". Il coordonne :
|
||||||
|
|
||||||
|
- le state global Zustand ;
|
||||||
|
- les étapes de mission ;
|
||||||
|
- les assets GLTF ;
|
||||||
|
- les prompts vidéo ;
|
||||||
|
- la mallette ;
|
||||||
|
- les interactions `E` ;
|
||||||
|
- les objets Rapier grabbables ;
|
||||||
|
- le scan des pièces ;
|
||||||
|
- la validation gameplay.
|
||||||
|
|
||||||
|
### Navigation simple
|
||||||
|
|
||||||
|
Ouvrir dans cet ordre :
|
||||||
|
|
||||||
|
1. `src/world/GameStageContent.tsx`
|
||||||
|
2. `src/components/three/gameplay/RepairGame.tsx`
|
||||||
|
3. `src/data/gameplay/repairMissions.ts`
|
||||||
|
4. `src/components/three/gameplay/RepairRepairingStep.tsx`
|
||||||
|
5. `src/managers/stores/useGameStore.ts`
|
||||||
|
|
||||||
|
### Comment expliquer l'architecture
|
||||||
|
|
||||||
|
`GameStageContent` place les missions dans le monde.
|
||||||
|
|
||||||
|
`RepairGame` reçoit une mission :
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<RepairGame mission="bike" position={[8, 0, -6]} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis il vérifie :
|
||||||
|
|
||||||
|
- est-ce que `mainState` correspond à cette mission ?
|
||||||
|
- quel est le `step` courant ?
|
||||||
|
- quelle config utiliser ?
|
||||||
|
- quel sous-composant monter ?
|
||||||
|
|
||||||
|
Les variations mission sont dans `repairMissions.ts` :
|
||||||
|
|
||||||
|
- modèle ;
|
||||||
|
- prompt vidéo ;
|
||||||
|
- pièces cassées ;
|
||||||
|
- bonnes pièces ;
|
||||||
|
- leurres ;
|
||||||
|
- timings ;
|
||||||
|
- placeholders.
|
||||||
|
|
||||||
|
### Pourquoi c'est bien
|
||||||
|
|
||||||
|
- Un seul flow réutilisable.
|
||||||
|
- Moins de duplication entre `bike`, `pylone`, `ferme`.
|
||||||
|
- Les règles générales restent dans les composants.
|
||||||
|
- Les variations restent dans la data.
|
||||||
|
- Le debug panel peut tester les mêmes steps que le vrai jeu.
|
||||||
|
|
||||||
|
### Compromis
|
||||||
|
|
||||||
|
Si une future mission devient très différente, la config ne suffira peut-être plus. Il faudra alors créer des composants spécifiques ou un vrai `MissionManager`.
|
||||||
|
|
||||||
|
### Points à dire si on ouvre `RepairRepairingStep`
|
||||||
|
|
||||||
|
Ce fichier gère l'étape la plus gameplay :
|
||||||
|
|
||||||
|
- pièces de remplacement ;
|
||||||
|
- pièces cassées à déposer ;
|
||||||
|
- snap vers placeholders ;
|
||||||
|
- feedback vert/rouge/bleu ;
|
||||||
|
- validation finale.
|
||||||
|
|
||||||
|
État local important :
|
||||||
|
|
||||||
|
- `placedPartIds`
|
||||||
|
- `depositedBrokenPartIds`
|
||||||
|
- `showBlockedInstallFeedback`
|
||||||
|
|
||||||
|
Phrase simple :
|
||||||
|
|
||||||
|
> Cette étape garde seulement l'état local de manipulation. La progression globale reste dans Zustand.
|
||||||
|
|
||||||
|
## Bloc principal 2 : Audio
|
||||||
|
|
||||||
|
### Ce qu'il faut comprendre
|
||||||
|
|
||||||
|
`AudioManager` est un singleton parce qu'il manipule des objets impératifs du navigateur :
|
||||||
|
|
||||||
|
- `HTMLAudioElement`
|
||||||
|
- `AudioContext`
|
||||||
|
- pools audio
|
||||||
|
- panner stereo
|
||||||
|
- musique active
|
||||||
|
|
||||||
|
### Navigation simple
|
||||||
|
|
||||||
|
Ouvrir dans cet ordre :
|
||||||
|
|
||||||
|
1. `src/managers/AudioManager.ts`
|
||||||
|
2. `src/world/GameMusic.tsx`
|
||||||
|
3. `src/managers/stores/useSettingsStore.ts`
|
||||||
|
4. `src/utils/dialogues/playDialogue.ts`
|
||||||
|
5. `src/components/ui/Subtitles.tsx`
|
||||||
|
|
||||||
|
### Architecture audio
|
||||||
|
|
||||||
|
Catégories :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
music
|
||||||
|
sfx
|
||||||
|
dialogue
|
||||||
|
```
|
||||||
|
|
||||||
|
Volumes :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
volume effectif = volume de base * volume catégorie
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
- `GameMusic` lance `/sounds/musique/test.mp3` avec base `0.33`.
|
||||||
|
- Si le volume musique settings vaut `0.5`, le volume effectif vaut `0.165`.
|
||||||
|
|
||||||
|
### Dialogues et sous-titres
|
||||||
|
|
||||||
|
Un dialogue référence :
|
||||||
|
|
||||||
|
- un `id` ;
|
||||||
|
- une voix ;
|
||||||
|
- un fichier audio ;
|
||||||
|
- un index de cue SRT.
|
||||||
|
|
||||||
|
Le SRT est par voix et par langue, pas un fichier SRT par dialogue.
|
||||||
|
|
||||||
|
Phrase simple :
|
||||||
|
|
||||||
|
> Les dialogues utilisent la catégorie audio `dialogue`, et `playDialogueById` synchronise le sous-titre actif avec le temps courant de l'audio.
|
||||||
|
|
||||||
|
### Risques à connaître
|
||||||
|
|
||||||
|
- Autoplay navigateur : la musique peut attendre un input utilisateur.
|
||||||
|
- Un seul track musique actif à la fois.
|
||||||
|
- Les dialogues sont queue-based, pas un vrai système de priorité.
|
||||||
|
- L'éditeur audio/SRT sauve via Vite local, pas en prod.
|
||||||
|
|
||||||
|
## Bloc principal 3 : Zustand
|
||||||
|
|
||||||
|
### Ce qu'il faut comprendre
|
||||||
|
|
||||||
|
Zustand contient l'état durable.
|
||||||
|
|
||||||
|
Il ne contient pas :
|
||||||
|
|
||||||
|
- la velocity joueur ;
|
||||||
|
- les raycasts ;
|
||||||
|
- les positions frame par frame ;
|
||||||
|
- les vecteurs temporaires ;
|
||||||
|
- l'état interne d'un grab.
|
||||||
|
|
||||||
|
### Navigation simple
|
||||||
|
|
||||||
|
Ouvrir dans cet ordre :
|
||||||
|
|
||||||
|
1. `src/managers/stores/useGameStore.ts`
|
||||||
|
2. `src/components/ui/debug/GameStateDebugPanel.tsx`
|
||||||
|
3. `src/components/three/gameplay/RepairGame.tsx`
|
||||||
|
4. `src/managers/stores/useSettingsStore.ts`
|
||||||
|
5. `src/managers/stores/useSubtitleStore.ts`
|
||||||
|
|
||||||
|
### Game store
|
||||||
|
|
||||||
|
Main states :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
intro
|
||||||
|
bike
|
||||||
|
pylone
|
||||||
|
ferme
|
||||||
|
outro
|
||||||
|
```
|
||||||
|
|
||||||
|
Mission steps :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
locked
|
||||||
|
waiting
|
||||||
|
inspected
|
||||||
|
fragmented
|
||||||
|
scanning
|
||||||
|
repairing
|
||||||
|
reassembling
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Actions importantes :
|
||||||
|
|
||||||
|
- `setMissionStep`
|
||||||
|
- `completeMission`
|
||||||
|
- `advanceGameState`
|
||||||
|
- `rewindGameState`
|
||||||
|
- `resetGame`
|
||||||
|
|
||||||
|
Phrase simple :
|
||||||
|
|
||||||
|
> Le debug panel et le vrai gameplay utilisent la même source de vérité. Ce n'est pas un debug state séparé.
|
||||||
|
|
||||||
|
### Settings store
|
||||||
|
|
||||||
|
Gère :
|
||||||
|
|
||||||
|
- menu ouvert/fermé ;
|
||||||
|
- volumes ;
|
||||||
|
- sous-titres ;
|
||||||
|
- langue ;
|
||||||
|
- `repairRuntime`.
|
||||||
|
|
||||||
|
Piège :
|
||||||
|
|
||||||
|
`repairRuntime` est stocké et affiché, mais pas encore utilisé par `RepairGame`.
|
||||||
|
|
||||||
|
### Subtitle store
|
||||||
|
|
||||||
|
Gère seulement :
|
||||||
|
|
||||||
|
- le sous-titre actif ;
|
||||||
|
- clear/set.
|
||||||
|
|
||||||
|
Phrase simple :
|
||||||
|
|
||||||
|
> Le store de sous-titres est volontairement petit parce que le timing reste piloté par l'audio.
|
||||||
|
|
||||||
|
## Les pièges à ne pas rater
|
||||||
|
|
||||||
|
Si on te pose une question précise, réponds vrai.
|
||||||
|
|
||||||
|
| Sujet | Réponse honnête |
|
||||||
|
| ------------------------- | ------------------------------------------------------------------------ |
|
||||||
|
| Lock mouvement réparation | Le hook existe mais retourne `false`, donc pas actif actuellement. |
|
||||||
|
| `repairRuntime` JS/Python | Le choix est stocké dans settings, mais pas consommé par le repair game. |
|
||||||
|
| Old debug flags | `noMusic`, `noMap`, `noDialogues`, etc. ne sont plus branchés. |
|
||||||
|
| Player physics | Le joueur n'est pas Rapier, il utilise capsule + octree. |
|
||||||
|
| Collision map | L'octree vient de nodes dédiés, actuellement `terrain`. |
|
||||||
|
| Editor save | Ce sont des endpoints Vite dev, pas une API de prod. |
|
||||||
|
| Cinematics | `GameCinematics` est monté seulement pendant `outro` dans `World`. |
|
||||||
|
| Hand tracking depth | Le `z` MediaPipe est relatif, pas une vraie profondeur monde stable. |
|
||||||
|
|
||||||
|
## Si l'évaluateur ouvre un fichier au hasard
|
||||||
|
|
||||||
|
| Fichier | Ce qu'il faut dire |
|
||||||
|
| -------------------------- | ----------------------------------------------------------------------------- |
|
||||||
|
| `World.tsx` | Compositeur de scène. Gère game/debug, loading gates, player spawn. |
|
||||||
|
| `useWorldSceneLoading.ts` | Évite de spawn le joueur avant map + collision + stage. |
|
||||||
|
| `GameMap.tsx` | Charge map JSON, modèles, fallback cubes, signale quand les nodes sont prêts. |
|
||||||
|
| `GameMapCollision.tsx` | Construit l'octree joueur depuis des nodes de collision dédiés. |
|
||||||
|
| `PlayerController.tsx` | Inputs, pointer lock, mouvement, jump, interaction, collision capsule. |
|
||||||
|
| `InteractionManager.ts` | État courant des interactions, pas dans Zustand car frame-adjacent. |
|
||||||
|
| `RepairGame.tsx` | Orchestrateur de steps repair, data-driven par mission. |
|
||||||
|
| `RepairRepairingStep.tsx` | Validation gameplay : pièces, snap, dépôt, install target. |
|
||||||
|
| `repairMissions.ts` | Config des missions, évite de hardcoder chaque mission dans le composant. |
|
||||||
|
| `AudioManager.ts` | Manager impératif pour musique, SFX, dialogue, pooling, volumes. |
|
||||||
|
| `playDialogue.ts` | Queue dialogue + synchronisation sous-titre. |
|
||||||
|
| `useGameStore.ts` | Source de vérité progression. |
|
||||||
|
| `GameStateDebugPanel.tsx` | Outil debug qui manipule le même store que le jeu. |
|
||||||
|
| `HandTrackingProvider.tsx` | Active la webcam seulement quand utile. |
|
||||||
|
| `GrabbableObject.tsx` | Grab souris/main, Rapier body, snap target. |
|
||||||
|
| `EditorControls.tsx` | Panneau éditeur, pas runtime joueur. |
|
||||||
|
|
||||||
|
## Démo live avec Chrome DevTools
|
||||||
|
|
||||||
|
Cette partie correspond à :
|
||||||
|
|
||||||
|
> Session de test / démo live via les devtools, commentée
|
||||||
|
|
||||||
|
Le but n'est pas de tout montrer. Le but est de montrer que tu sais observer l'application proprement.
|
||||||
|
|
||||||
|
### Préparation
|
||||||
|
|
||||||
|
Lancer le front :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Ouvrir :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
http://localhost:5173/?debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Si Vite affiche un autre port, utiliser ce port.
|
||||||
|
|
||||||
|
Ouvrir DevTools :
|
||||||
|
|
||||||
|
- macOS : `Cmd + Option + I`
|
||||||
|
- Windows/Linux : `Ctrl + Shift + I`
|
||||||
|
|
||||||
|
Disposition conseillée :
|
||||||
|
|
||||||
|
- app à gauche ;
|
||||||
|
- DevTools docké à droite ;
|
||||||
|
- onglets prêts : `Console`, `Network`, `Sources`, `Application`.
|
||||||
|
|
||||||
|
### Étape 1 : Console
|
||||||
|
|
||||||
|
Objectif :
|
||||||
|
|
||||||
|
- vérifier que l'app ne crashe pas ;
|
||||||
|
- surveiller les erreurs quand on interagit.
|
||||||
|
|
||||||
|
À faire :
|
||||||
|
|
||||||
|
1. Ouvrir `Console`.
|
||||||
|
2. Reload la page.
|
||||||
|
3. Vérifier s'il y a des erreurs rouges.
|
||||||
|
4. Garder la console ouverte pendant la démo.
|
||||||
|
|
||||||
|
Phrase simple :
|
||||||
|
|
||||||
|
> Je commence par la console pour vérifier que la démo ne cache pas une erreur runtime.
|
||||||
|
|
||||||
|
### Étape 2 : Network
|
||||||
|
|
||||||
|
Objectif :
|
||||||
|
|
||||||
|
- montrer que les assets sont chargés ;
|
||||||
|
- vérifier map, modèles, sons, vidéos.
|
||||||
|
|
||||||
|
À faire :
|
||||||
|
|
||||||
|
1. Ouvrir `Network`.
|
||||||
|
2. Cocher `Disable cache`.
|
||||||
|
3. Reload.
|
||||||
|
4. Filtrer :
|
||||||
|
- `map.json`
|
||||||
|
- `model.gltf`
|
||||||
|
- `.webm`
|
||||||
|
- `.mp3`
|
||||||
|
- `.srt`
|
||||||
|
|
||||||
|
Ce que tu peux expliquer :
|
||||||
|
|
||||||
|
- `map.json` décrit la scène ;
|
||||||
|
- `model.gltf` charge les assets 3D ;
|
||||||
|
- `.webm` sert aux prompts in-game ;
|
||||||
|
- `.mp3` sert à musique/dialogues/SFX ;
|
||||||
|
- `.srt` sert aux sous-titres.
|
||||||
|
|
||||||
|
Phrase simple :
|
||||||
|
|
||||||
|
> Network me permet de vérifier que la feature n'est pas juste du code React, elle dépend aussi d'assets runtime.
|
||||||
|
|
||||||
|
### Étape 3 : Sources pour suivre le repair game
|
||||||
|
|
||||||
|
Objectif :
|
||||||
|
|
||||||
|
- montrer une transition de step ;
|
||||||
|
- prouver que `RepairGame` écrit dans Zustand.
|
||||||
|
|
||||||
|
À faire :
|
||||||
|
|
||||||
|
1. Ouvrir `Sources`.
|
||||||
|
2. `Cmd + P` ou `Ctrl + P`.
|
||||||
|
3. Chercher `RepairGame.tsx`.
|
||||||
|
4. Mettre un breakpoint sur un appel à `setMissionStep`.
|
||||||
|
5. Chercher `useGameStore.ts`.
|
||||||
|
6. Mettre un breakpoint dans `setMissionStep` ou `completeMission`.
|
||||||
|
|
||||||
|
Dans l'app :
|
||||||
|
|
||||||
|
1. Activer `?debug`.
|
||||||
|
2. Dans lil-gui, mettre `Scene = Physics`.
|
||||||
|
3. Garder `Camera Mode = Player`.
|
||||||
|
4. Dans le debug overlay, passer `Main state = Bike`.
|
||||||
|
5. Mettre le sub-state sur `waiting` si besoin.
|
||||||
|
6. Interagir avec l'objet ou avancer les steps via le debug panel.
|
||||||
|
|
||||||
|
Quand le breakpoint pause :
|
||||||
|
|
||||||
|
- regarder `mission` ;
|
||||||
|
- regarder `step` ;
|
||||||
|
- expliquer la transition.
|
||||||
|
|
||||||
|
Phrase simple :
|
||||||
|
|
||||||
|
> Là on voit que le composant ne change pas juste son état local. Il déclenche une transition dans le store global.
|
||||||
|
|
||||||
|
### Étape 4 : Sources pour suivre l'audio
|
||||||
|
|
||||||
|
Objectif :
|
||||||
|
|
||||||
|
- montrer qu'un son passe par `AudioManager`.
|
||||||
|
|
||||||
|
À faire :
|
||||||
|
|
||||||
|
1. Ouvrir `AudioManager.ts`.
|
||||||
|
2. Mettre un breakpoint dans `playSound`.
|
||||||
|
3. Déclencher un son, par exemple ouverture/fermeture de mallette.
|
||||||
|
4. Inspecter :
|
||||||
|
- `path`
|
||||||
|
- `volume`
|
||||||
|
- `options.category`
|
||||||
|
|
||||||
|
Ensuite :
|
||||||
|
|
||||||
|
1. Ouvrir le menu avec `Escape`.
|
||||||
|
2. Changer le volume SFX.
|
||||||
|
3. Mettre un breakpoint dans `setCategoryVolume`.
|
||||||
|
4. Vérifier que la catégorie change.
|
||||||
|
|
||||||
|
Phrase simple :
|
||||||
|
|
||||||
|
> Les sliders ne modifient pas directement un audio isolé. Ils mettent à jour un volume de catégorie dans le manager.
|
||||||
|
|
||||||
|
### Étape 5 : Application
|
||||||
|
|
||||||
|
Objectif :
|
||||||
|
|
||||||
|
- montrer les données locales du navigateur.
|
||||||
|
|
||||||
|
À faire :
|
||||||
|
|
||||||
|
1. Ouvrir `Application`.
|
||||||
|
2. Regarder `Local Storage`.
|
||||||
|
3. Chercher la clé de debug :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
la-fabrik-debug-controls
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce que ça montre :
|
||||||
|
|
||||||
|
- certains choix debug peuvent être persistés ;
|
||||||
|
- ce n'est pas la progression du jeu ;
|
||||||
|
- c'est juste du confort dev.
|
||||||
|
|
||||||
|
### Étape 6 : Performance, optionnel
|
||||||
|
|
||||||
|
Objectif :
|
||||||
|
|
||||||
|
- montrer que tu sais diagnostiquer si ça rame.
|
||||||
|
|
||||||
|
À faire seulement si demandé :
|
||||||
|
|
||||||
|
1. Ouvrir `Performance`.
|
||||||
|
2. Record 5 secondes.
|
||||||
|
3. Bouger dans la scène.
|
||||||
|
4. Stop.
|
||||||
|
5. Observer FPS, scripting, rendering.
|
||||||
|
|
||||||
|
Phrase simple :
|
||||||
|
|
||||||
|
> Si la scène rame, je ne devine pas. Je regarde si le temps part dans le JS, le rendu, ou le chargement d'assets.
|
||||||
|
|
||||||
|
## Démo recommandée en 5 minutes
|
||||||
|
|
||||||
|
### 1. Ouvrir la scène debug
|
||||||
|
|
||||||
|
```txt
|
||||||
|
http://localhost:5173/?debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Dans lil-gui :
|
||||||
|
|
||||||
|
- `Scene = Physics`
|
||||||
|
- `Camera Mode = Player`
|
||||||
|
- `Debug Overlay = true`
|
||||||
|
|
||||||
|
### 2. Montrer le store
|
||||||
|
|
||||||
|
Dans le debug panel :
|
||||||
|
|
||||||
|
- passer `Main state = Bike`
|
||||||
|
- passer le step à `waiting`
|
||||||
|
- avancer avec `Next step`
|
||||||
|
|
||||||
|
Expliquer :
|
||||||
|
|
||||||
|
> Le panel debug écrit dans le même store que le vrai gameplay.
|
||||||
|
|
||||||
|
### 3. Montrer le repair game
|
||||||
|
|
||||||
|
Faire défiler les steps :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
waiting -> inspected -> fragmented -> scanning -> repairing
|
||||||
|
```
|
||||||
|
|
||||||
|
Montrer :
|
||||||
|
|
||||||
|
- objet de mission ;
|
||||||
|
- mallette ;
|
||||||
|
- modèle éclaté ;
|
||||||
|
- scan ;
|
||||||
|
- pièces à grab ;
|
||||||
|
- validation bloquée si mauvaise pièce.
|
||||||
|
|
||||||
|
### 4. Montrer l'audio
|
||||||
|
|
||||||
|
Ouvrir DevTools :
|
||||||
|
|
||||||
|
- breakpoint dans `AudioManager.playSound` ;
|
||||||
|
- déclencher un son ;
|
||||||
|
- montrer `path` et `category`.
|
||||||
|
|
||||||
|
### 5. Montrer Zustand
|
||||||
|
|
||||||
|
Breakpoint dans `useGameStore`.
|
||||||
|
|
||||||
|
Expliquer :
|
||||||
|
|
||||||
|
> Quand la mission avance, ce n'est pas chaque composant qui invente son état. La progression passe par des actions centralisées.
|
||||||
|
|
||||||
|
## Questions probables et réponses simples
|
||||||
|
|
||||||
|
### Pourquoi Zustand ?
|
||||||
|
|
||||||
|
Parce qu'on a besoin d'une source de vérité partagée entre UI, monde 3D, debug panel et gameplay.
|
||||||
|
|
||||||
|
### Pourquoi pas tout dans Zustand ?
|
||||||
|
|
||||||
|
Parce que certaines valeurs changent trop souvent. Les positions, vitesses, raycasts et animations frame par frame restent dans des refs ou dans les composants R3F.
|
||||||
|
|
||||||
|
### Pourquoi un `AudioManager` ?
|
||||||
|
|
||||||
|
Parce que l'audio navigateur est impératif. On doit gérer des `HTMLAudioElement`, des pools, une musique active et des volumes de catégories.
|
||||||
|
|
||||||
|
### Pourquoi séparer octree et Rapier ?
|
||||||
|
|
||||||
|
Pour garder un player controller simple tout en utilisant Rapier pour les objets manipulables.
|
||||||
|
|
||||||
|
### Pourquoi `RepairGame` est data-driven ?
|
||||||
|
|
||||||
|
Pour réutiliser le même flow sur plusieurs missions et garder les variations dans `repairMissions.ts`.
|
||||||
|
|
||||||
|
### Qu'est-ce qui est incomplet ?
|
||||||
|
|
||||||
|
- pas de vrai `GameManager` global ;
|
||||||
|
- lock mouvement réparation désactivé ;
|
||||||
|
- `repairRuntime` pas consommé ;
|
||||||
|
- editor save uniquement en dev ;
|
||||||
|
- hand tracking encore approximatif sur profondeur et smoothing.
|
||||||
|
|
||||||
|
## Checklist avant la review
|
||||||
|
|
||||||
|
Commandes :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run format:check
|
||||||
|
npm run typecheck
|
||||||
|
npm run lint
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Pages à ouvrir :
|
||||||
|
|
||||||
|
- `/`
|
||||||
|
- `/?debug`
|
||||||
|
- `/editor`
|
||||||
|
- `/docs`
|
||||||
|
- `/docs/code-review`
|
||||||
|
|
||||||
|
Fichiers à avoir en tête :
|
||||||
|
|
||||||
|
- `src/world/World.tsx`
|
||||||
|
- `src/components/three/gameplay/RepairGame.tsx`
|
||||||
|
- `src/components/three/gameplay/RepairRepairingStep.tsx`
|
||||||
|
- `src/data/gameplay/repairMissions.ts`
|
||||||
|
- `src/managers/AudioManager.ts`
|
||||||
|
- `src/managers/stores/useGameStore.ts`
|
||||||
|
- `src/components/ui/debug/GameStateDebugPanel.tsx`
|
||||||
|
|
||||||
|
Réponses pièges à réviser :
|
||||||
|
|
||||||
|
- lock mouvement repair désactivé actuellement ;
|
||||||
|
- `repairRuntime` pas consommé ;
|
||||||
|
- player pas Rapier ;
|
||||||
|
- save editor pas production ;
|
||||||
|
- old boot flags non branchés.
|
||||||
|
|
||||||
|
## Mini plan de révision
|
||||||
|
|
||||||
|
### Session 1 : 20 minutes
|
||||||
|
|
||||||
|
Lire :
|
||||||
|
|
||||||
|
- cette fiche ;
|
||||||
|
- `docs/technical/repair-game.md` ;
|
||||||
|
- `docs/technical/zustand.md`.
|
||||||
|
|
||||||
|
Objectif :
|
||||||
|
|
||||||
|
- savoir expliquer le repair game et le store.
|
||||||
|
|
||||||
|
### Session 2 : 20 minutes
|
||||||
|
|
||||||
|
Lire :
|
||||||
|
|
||||||
|
- `docs/technical/audio.md` ;
|
||||||
|
- `docs/technical/interaction.md`.
|
||||||
|
|
||||||
|
Objectif :
|
||||||
|
|
||||||
|
- savoir expliquer audio + interaction.
|
||||||
|
|
||||||
|
### Session 3 : 20 minutes
|
||||||
|
|
||||||
|
Faire la démo :
|
||||||
|
|
||||||
|
- lancer `npm run dev` ;
|
||||||
|
- ouvrir `/?debug` ;
|
||||||
|
- ouvrir DevTools ;
|
||||||
|
- faire un breakpoint dans `RepairGame` ;
|
||||||
|
- faire un breakpoint dans `AudioManager`.
|
||||||
|
|
||||||
|
Objectif :
|
||||||
|
|
||||||
|
- ne pas découvrir DevTools le jour de la review.
|
||||||
|
|
||||||
|
## Version ultra-courte à garder en tête
|
||||||
|
|
||||||
|
Si tu paniques, reviens à ça :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
World monte la scène.
|
||||||
|
GameStageContent place les missions.
|
||||||
|
RepairGame orchestre les steps.
|
||||||
|
repairMissions fournit la data.
|
||||||
|
useGameStore garde la progression.
|
||||||
|
InteractionManager gère focus/trigger/grab.
|
||||||
|
AudioManager gère sons, musique, dialogues.
|
||||||
|
DevTools permet de suivre assets, state et appels runtime.
|
||||||
|
```
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Animation & 3D Components
|
||||||
|
|
||||||
|
This document describes the 3D components that are currently used in the runtime.
|
||||||
|
|
||||||
|
## Runtime Components
|
||||||
|
|
||||||
|
| Domain | Component | Role |
|
||||||
|
| ----------- | -------------------- | --------------------------------------------------------------------- |
|
||||||
|
| 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
|
||||||
|
|
||||||
|
Use `useFrame` for per-frame 3D behavior. Current examples:
|
||||||
|
|
||||||
|
- `GrabbableObject` updates held object velocity every frame.
|
||||||
|
- `ExplodableModel` updates split part positions every frame.
|
||||||
|
- `RepairCaseModel` updates proximity float and rotation wobble every frame.
|
||||||
|
- `SkyModel` follows the camera position every frame.
|
||||||
|
|
||||||
|
## Timeline Animation
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/components/three/
|
||||||
|
├── gameplay/
|
||||||
|
│ ├── RepairCaseModel.tsx
|
||||||
|
│ ├── RepairGame.tsx
|
||||||
|
│ └── RepairRepairingStep.tsx
|
||||||
|
├── interaction/
|
||||||
|
│ ├── GrabbableObject.tsx
|
||||||
|
│ ├── InteractableObject.tsx
|
||||||
|
│ └── TriggerObject.tsx
|
||||||
|
├── models/
|
||||||
|
│ ├── AnimatedModel.tsx
|
||||||
|
│ ├── ExplodableModel.tsx
|
||||||
|
│ └── SimpleModel.tsx
|
||||||
|
└── world/
|
||||||
|
└── SkyModel.tsx
|
||||||
|
```
|
||||||
+288
-31
@@ -4,44 +4,301 @@ This document describes the code that exists today in the repository.
|
|||||||
|
|
||||||
## Runtime Structure
|
## Runtime Structure
|
||||||
|
|
||||||
- `src/App.tsx` mounts the `Canvas`, the 3D `World`, the debug perf overlay, and the HTML overlays.
|
- `src/main.tsx` mounts React in `StrictMode`.
|
||||||
- `src/world/World.tsx` composes the active scene, including:
|
- `src/App.tsx` mounts TanStack `RouterProvider`.
|
||||||
- environment and lighting
|
- `src/router.tsx` declares `/`, `/editor`, and `/docs`.
|
||||||
- debug helpers and debug camera mode
|
- `src/pages/page.tsx` composes the playable route with `HandTrackingProvider`, React Three Fiber `Canvas`, `World`, `DebugPerf`, `GameUI`, and `SceneLoadingOverlay`.
|
||||||
- either the map scene or the debug physics test scene
|
- `src/pages/editor/page.tsx` composes the local editor route.
|
||||||
- the player rig when the active camera mode is `player`
|
- `src/components/docs/DocsLayout.tsx` composes the in-app documentation route.
|
||||||
- `src/world/Map.tsx` loads the main map model and builds the collision octree.
|
|
||||||
- `src/world/debug/TestScene.tsx` provides a debug-oriented interaction and physics scene.
|
Detailed runtime-loading notes live in `docs/technical/scene-runtime.md`.
|
||||||
- `src/world/player/PlayerComponent.tsx` mounts the camera and controller.
|
|
||||||
- `src/world/player/PlayerController.tsx` owns pointer lock movement, jump handling, and interaction input.
|
## 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 systems with separate responsibilities:
|
||||||
|
|
||||||
|
- Player movement uses a Three.js `Capsule` and an `Octree`.
|
||||||
|
- Gameplay objects use Rapier rigid bodies and colliders.
|
||||||
|
|
||||||
|
`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
|
## Interaction Model
|
||||||
|
|
||||||
- `src/stateManager/InteractionManager.ts` is the current interaction state source.
|
Core interaction files:
|
||||||
- `src/components/3d/InteractableObject.tsx` handles focus detection through distance and raycasting.
|
|
||||||
- `src/components/3d/TriggerObject.tsx` implements trigger-style interactions.
|
|
||||||
- `src/components/3d/GrabbableObject.tsx` implements hold-and-release interactions.
|
|
||||||
- `src/hooks/useInteraction.ts` exposes the interaction snapshot to React UI.
|
|
||||||
- `src/components/ui/InteractPrompt.tsx` shows the `E` prompt for trigger interactions.
|
|
||||||
|
|
||||||
## 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/stateManager/AudioManager.ts` currently provides pooled one-shot sound playback.
|
The player controller bridges raw input to semantic interaction actions:
|
||||||
- Trigger interactions may play audio directly through `AudioManager`.
|
|
||||||
|
|
||||||
## Debug System
|
- `E` triggers focused trigger objects
|
||||||
|
- primary mouse button grabs focused grabbable objects
|
||||||
|
- hand tracking can grab hand-controlled grabbable objects
|
||||||
|
|
||||||
- Debug mode is enabled with `?debug`.
|
Detailed interaction notes live in `docs/technical/interaction.md`.
|
||||||
- `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.
|
## Audio, Dialogue, And Subtitles
|
||||||
- `src/utils/debug/DebugPerf.tsx` lazily mounts `r3f-perf` in debug mode.
|
|
||||||
- `src/utils/debug/scene/DebugHelpers.tsx` mounts debug helpers.
|
Audio is split into:
|
||||||
- `src/utils/debug/scene/DebugCameraControls.tsx` mounts the free debug camera.
|
|
||||||
|
- `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
|
||||||
|
|
||||||
|
Runtime cinematic data lives in:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
public/cinematics.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Cinematics support camera keyframes, GSAP timelines, optional dialogue cues, and `isCinematicPlaying` input locking. Current world integration mounts `GameCinematics` only during the outro state.
|
||||||
|
|
||||||
|
## Hand Tracking
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
## Current Limitations
|
||||||
|
|
||||||
- The repository is still a prototype, not the full intended game runtime.
|
- The repository is still a prototype.
|
||||||
- `src/world/debug/TestScene.tsx` is still part of the active scene composition.
|
- There is no central production `GameManager`.
|
||||||
- There is no central gameplay orchestrator such as `GameManager` yet.
|
- The repair game is implemented, but broader mission orchestration is still light.
|
||||||
- Missions, zones, cinematics, and dialogue systems are not implemented.
|
- `useRepairMovementLocked()` currently returns `false`, so repair movement lock is disabled even though the rule and UI component exist.
|
||||||
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
|
- 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.
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# Editor Technical Notes
|
||||||
|
|
||||||
|
This document describes the map editor that exists in the current codebase.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The editor is a React route used to inspect and adjust the `public/map.json` scene data from inside the La-Fabrik app. It shares the same `MapNode` data format as the game scene and uses React Three Fiber for rendering.
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
- `/` renders the playable La-Fabrik scene.
|
||||||
|
- `/editor` renders the map editor.
|
||||||
|
- `src/App.tsx` mounts TanStack Router through `RouterProvider`.
|
||||||
|
- `src/router.tsx` defines the `/editor` route and imports `EditorPage` from `src/pages/editor/page.tsx`.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/
|
||||||
|
├── pages/
|
||||||
|
│ └── editor/
|
||||||
|
│ └── page.tsx
|
||||||
|
├── components/
|
||||||
|
│ └── editor/
|
||||||
|
│ ├── EditorControls.tsx
|
||||||
|
│ ├── EditorCinematicManifestPanel.tsx
|
||||||
|
│ ├── EditorDialogueManifestPanel.tsx
|
||||||
|
│ ├── EditorSrtPanel.tsx
|
||||||
|
│ └── scene/
|
||||||
|
│ ├── EditorMap.tsx
|
||||||
|
│ └── EditorScene.tsx
|
||||||
|
├── controls/
|
||||||
|
│ └── editor/
|
||||||
|
│ └── FlyController.tsx
|
||||||
|
├── hooks/
|
||||||
|
│ └── editor/
|
||||||
|
│ ├── useEditorHistory.ts
|
||||||
|
│ └── useEditorSceneData.ts
|
||||||
|
├── types/
|
||||||
|
│ └── editor/
|
||||||
|
│ └── editor.ts
|
||||||
|
└── utils/
|
||||||
|
├── dialogues/
|
||||||
|
│ └── loadDialogueManifest.ts
|
||||||
|
├── editor/
|
||||||
|
│ └── loadEditorScene.ts
|
||||||
|
├── map/
|
||||||
|
│ └── loadMapSceneData.ts
|
||||||
|
└── subtitles/
|
||||||
|
└── parseSrt.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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, 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.
|
||||||
|
|
||||||
|
`src/hooks/editor/useEditorHistory.ts` owns editor undo and redo history.
|
||||||
|
|
||||||
|
`src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, keyboard shortcuts, and `EditorMap`.
|
||||||
|
|
||||||
|
`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. 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.
|
||||||
|
|
||||||
|
`src/components/editor/EditorCinematicManifestPanel.tsx` renders the cinematic manifest editor. It loads `cinematics.json`, edits camera keyframes and dialogue cues, previews selected cinematics in the editor canvas, and saves the manifest through a dev-server endpoint.
|
||||||
|
|
||||||
|
`src/components/editor/EditorSrtPanel.tsx` renders the dialogue subtitle editor inside the control panel. It loads the dialogue manifest, loads one SRT file per voice/language, validates cue structure, previews dialogue audio, and can save SRT files through a dev-server endpoint.
|
||||||
|
|
||||||
|
`src/controls/editor/FlyController.tsx` provides editor movement controls for player-style navigation.
|
||||||
|
|
||||||
|
`src/utils/map/loadMapSceneData.ts` is shared by the game map and editor. It loads `/map.json` and resolves available `public/models/{name}/model.glb` files first, then falls back to `public/models/{name}/model.gltf`.
|
||||||
|
|
||||||
|
`src/utils/editor/loadEditorScene.ts` contains editor-only upload handling for user-selected folders.
|
||||||
|
|
||||||
|
## Data Format
|
||||||
|
|
||||||
|
The shared editor type lives in `src/types/editor/editor.ts`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface MapNode {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
position: [number, number, number];
|
||||||
|
rotation: [number, number, number];
|
||||||
|
scale: [number, number, number];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`public/map.json` is expected to be a `MapNode[]`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "pylone",
|
||||||
|
"type": "Mesh",
|
||||||
|
"position": [0, 5, 0],
|
||||||
|
"rotation": [0, 1.57, 0],
|
||||||
|
"scale": [1, 1, 1]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Each node `name` maps to a model folder:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
public/
|
||||||
|
├── map.json
|
||||||
|
└── models/
|
||||||
|
└── pylone/
|
||||||
|
└── model.glb
|
||||||
|
```
|
||||||
|
|
||||||
|
If `model.glb` and `model.gltf` are both missing, the editor renders a fallback cube so the node can still be selected and transformed.
|
||||||
|
|
||||||
|
## Editor Flow
|
||||||
|
|
||||||
|
1. `EditorPage` mounts on `/editor`.
|
||||||
|
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. `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.
|
||||||
|
- `Ctrl+Z` or `Cmd+Z`: undo.
|
||||||
|
- `Ctrl+Y` or `Cmd+Y`: redo.
|
||||||
|
- `WASD`, `ZQSD`, or arrow keys: move in player-controller mode.
|
||||||
|
- `Space`: move upward in player-controller mode.
|
||||||
|
- `Shift`: move downward in player-controller mode.
|
||||||
|
|
||||||
|
## Saving And Exporting
|
||||||
|
|
||||||
|
The editor supports two output paths:
|
||||||
|
|
||||||
|
- Export JSON downloads the current `MapNode[]` as `map.json`.
|
||||||
|
- Save to Server posts the current `MapNode[]` to `/api/save-map`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Runtime dialogue files are grouped under `public/sounds/dialogue/`:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
public/
|
||||||
|
└── sounds/
|
||||||
|
└── dialogue/
|
||||||
|
├── dialogues.json
|
||||||
|
└── subtitles/
|
||||||
|
├── fr/
|
||||||
|
│ ├── narrateur.srt
|
||||||
|
│ ├── fermier.srt
|
||||||
|
│ └── electricienne.srt
|
||||||
|
└── en/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
The current model is one SRT file per voice and language. A dialogue entry references the cue it needs through `subtitleCueIndex`; it does not own a dedicated SRT file.
|
||||||
|
|
||||||
|
`EditorSrtPanel` uses:
|
||||||
|
|
||||||
|
- `loadDialogueManifest()` to read `/sounds/dialogue/dialogues.json`
|
||||||
|
- `parseSrt()` to validate local textarea content and find active cues during audio preview
|
||||||
|
- `/api/save-srt` to write edited SRT files during local development
|
||||||
|
- `/api/validate-dialogues` to validate the manifest, linked audio, French SRT files, and referenced cue indexes
|
||||||
|
|
||||||
|
SRT timecodes are relative to the dialogue audio file being previewed, not to the global game timeline.
|
||||||
|
|
||||||
|
Missing English SRT files are warnings, not errors, because runtime loading falls back to French subtitles when the selected language is not available. Keep this behavior until the English translation workflow is ready.
|
||||||
|
|
||||||
|
## Dialogue Manifest Editing
|
||||||
|
|
||||||
|
`EditorDialogueManifestPanel` edits `public/sounds/dialogue/dialogues.json` in memory and persists it through `/api/save-dialogues`.
|
||||||
|
|
||||||
|
The panel supports:
|
||||||
|
|
||||||
|
- adding a dialogue entry
|
||||||
|
- deleting a dialogue entry
|
||||||
|
- editing `id`, `voice`, `audio`, `subtitleCueIndex`, and optional `timecode`
|
||||||
|
- previewing the selected dialogue through `playDialogueById()`
|
||||||
|
- creating a missing French SRT cue through `/api/save-srt`
|
||||||
|
|
||||||
|
When a dialogue is added, the editor computes the next `subtitleCueIndex` for the selected voice from the manifest. The generated SRT cue is a valid placeholder block and should be edited later in the SRT panel.
|
||||||
|
|
||||||
|
`/api/save-dialogues` is implemented in `vite.config.ts`. It validates manifest shape before writing to `public/sounds/dialogue/dialogues.json`.
|
||||||
|
|
||||||
|
## Cinematic Manifest Editing
|
||||||
|
|
||||||
|
`EditorCinematicManifestPanel` edits `public/cinematics.json` in memory and persists it through `/api/save-cinematics`.
|
||||||
|
|
||||||
|
The manifest shape is:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CinematicDefinition {
|
||||||
|
id: string;
|
||||||
|
timecode?: number;
|
||||||
|
cameraKeyframes: CinematicCameraKeyframe[];
|
||||||
|
dialogueCues?: CinematicDialogueCue[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`cameraKeyframes` are relative to the cinematic start. At least two keyframes are required and keyframe times must increase.
|
||||||
|
|
||||||
|
`dialogueCues` are also relative to the cinematic start and reference dialogue IDs from `dialogues.json`. They are used by `GameCinematics` to synchronize dialogue playback with camera timelines. A dialogue synchronized this way should not also define a global `timecode` in `dialogues.json`.
|
||||||
|
|
||||||
|
The editor preview sends the selected `CinematicDefinition` to `EditorScene`, where GSAP animates the current editor camera. Orbit and fly controls are disabled during preview.
|
||||||
|
|
||||||
|
`/api/save-cinematics` is implemented in `vite.config.ts`. It validates manifest shape before writing to `public/cinematics.json`.
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
Editor styles are in `src/index.css` under the `/* Editor page */` section. Classes are prefixed with `editor-` to avoid collisions with the game UI.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- Uploaded model object URLs are not revoked after replacement or unmount.
|
||||||
|
- Large `map.json` files are not virtualized, culled, or LOD-managed.
|
||||||
|
- There is no snap-to-grid, duplication, material editing, or object creation workflow.
|
||||||
|
- Save to Server is a Vite dev-server helper, not a production backend API.
|
||||||
|
- SRT Save is also a Vite dev-server helper, not a production backend API.
|
||||||
|
- Dialogue and cinematic manifest saves are Vite dev-server helpers, not production backend APIs.
|
||||||
|
- Dialogue creation still uses placeholder audio paths until real MP3 files are added.
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# Game Flow - La Fabrik
|
||||||
|
|
||||||
|
## Étapes du jeu
|
||||||
|
|
||||||
|
```
|
||||||
|
intro → start-intro → naming → bienvenue → star-move → mission2 → searching_problem → preparation → outOfFabrik
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Détail des étapes
|
||||||
|
|
||||||
|
### 1. `intro` (initial)
|
||||||
|
|
||||||
|
- État initial au chargement du jeu
|
||||||
|
- Aucune action, juste une étape de départ
|
||||||
|
- Transition automatique vers `start-intro`
|
||||||
|
|
||||||
|
### 2. `start-intro`
|
||||||
|
|
||||||
|
- **Déclenchement** : Auto-transition depuis `intro` quand la scène est chargée
|
||||||
|
- **Action** : Joue l'audio d'intro (`intro`)
|
||||||
|
- **Attente** : Attend que l'audio se termine
|
||||||
|
- **Transition** : Vers `naming` quand l'audio se termine
|
||||||
|
|
||||||
|
### 3. `naming`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand l'audio d'intro se termine
|
||||||
|
- **Action** : Affiche un input pour demander le prénom du joueur
|
||||||
|
- **Attente** : L'utilisateur entre son prénom et valide
|
||||||
|
- **Transition** : Vers `bienvenue` quand l'utilisateur valide
|
||||||
|
|
||||||
|
### 4. `bienvenue`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand l'utilisateur valide son prénom
|
||||||
|
- **Actions** :
|
||||||
|
- Affiche "Bienvenue {prénom} !" à l'écran
|
||||||
|
- Joue l'audio de bienvenue
|
||||||
|
- **Attente** : Attend que l'audio se termine
|
||||||
|
- **Transition** : Vers `star-move` quand l'audio se termine
|
||||||
|
|
||||||
|
### 5. `star-move`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand l'audio de bienvenue se termine
|
||||||
|
- **Action** : Active le mouvement du joueur (`setCanMove(true)`)
|
||||||
|
- **État** : Le joueur peut maintenant se déplacer librement
|
||||||
|
- **Zone** : La détection de zone devient active (ZoneDetection)
|
||||||
|
|
||||||
|
### 6. `mission2`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand le joueur entre dans la zone `fabrikExit` (position: `[-5, 25, -15]`)
|
||||||
|
- **Actions** :
|
||||||
|
- Stocke `activityCity: false` dans le store Zustand
|
||||||
|
- Joue l'audio `alertCentral`
|
||||||
|
- **État** : Les objets avec hook `useActivityCity()` détectent le changement et jouent leurs animations
|
||||||
|
- **Attente** : Le joueur atteint la zone de trigger pour `searching_problem`
|
||||||
|
|
||||||
|
### 7. `searching_problem`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand le joueur entre dans la zone `searchingProblemZone` (position: `[-5, 25, -30]`)
|
||||||
|
- **Actions** :
|
||||||
|
- Joue l'audio `searchingProblem`
|
||||||
|
- Affiche l'objet "central" (position: `[1, 15, -45]`)
|
||||||
|
- **Attente** : Le joueur interagit avec l'objet "central"
|
||||||
|
|
||||||
|
### 8. `preparation`
|
||||||
|
|
||||||
|
- **Déclenchement** : Quand le joueur interagit avec l'objet "central"
|
||||||
|
- **Actions** :
|
||||||
|
- Bloque le mouvement (`setCanMove(false)`)
|
||||||
|
- Cache l'objet "central"
|
||||||
|
|
||||||
|
### 9. `outOfFabrik`
|
||||||
|
|
||||||
|
- **Déclenchement** : (non implémenté pour le moment)
|
||||||
|
- **Action** : Transition vers l'étape finale
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fichiers clés
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
| --------------------------------------- | --------------------------------------------------------- |
|
||||||
|
| `src/stores/gameStore.ts` | Store Zustand pour l'état global du jeu |
|
||||||
|
| `src/stateManager/GameStepManager.ts` | Synchronise avec le store Zustand |
|
||||||
|
| `src/components/game/GameFlow.tsx` | Gère les transitions automatiques et la lecture audio |
|
||||||
|
| `src/components/ui/IntroUI.tsx` | Affiche l'input pour le prénom et le message de bienvenue |
|
||||||
|
| `src/components/zone/ZoneDetection.tsx` | Détecte quand le joueur entre dans une zone |
|
||||||
|
| `src/components/3d/CentralObject.tsx` | Objet interactif "central" pour la mission 2 |
|
||||||
|
| `src/data/audioConfig.ts` | Chemins des fichiers audio |
|
||||||
|
| `src/data/zones.ts` | Configuration des zones de transition |
|
||||||
|
| `src/hooks/useActivityCity.ts` | Hook pour détecter le changement d'activité de la ville |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration audio
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/data/audioConfig.ts
|
||||||
|
export const AUDIO_PATHS = {
|
||||||
|
intro: "/sounds/fa.mp3",
|
||||||
|
bienvenue: "/sounds/fa.mp3",
|
||||||
|
alertCentral: "/sounds/fa.mp3",
|
||||||
|
searchingProblem: "/sounds/fa.mp3",
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration des zones
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/data/zones.ts
|
||||||
|
export const ZONES: Zone[] = [
|
||||||
|
{
|
||||||
|
id: "fabrikExit",
|
||||||
|
position: [-5, 25, -15],
|
||||||
|
radius: 10,
|
||||||
|
height: 20,
|
||||||
|
targetStep: "mission2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "searchingProblemZone",
|
||||||
|
position: [-5, 25, -30],
|
||||||
|
radius: 10,
|
||||||
|
height: 20,
|
||||||
|
targetStep: "searching_problem",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Store Zustand
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/stores/gameStore.ts
|
||||||
|
interface GameState {
|
||||||
|
step: GameStep;
|
||||||
|
activityCity: boolean;
|
||||||
|
playerName: string;
|
||||||
|
canMove: boolean;
|
||||||
|
setStep: (step: GameStep) => void;
|
||||||
|
setActivityCity: (value: boolean) => void;
|
||||||
|
setPlayerName: (name: string) => void;
|
||||||
|
setCanMove: (canMove: boolean) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hooks personnalisés
|
||||||
|
|
||||||
|
### useActivityCity
|
||||||
|
|
||||||
|
Permet aux objets 3D de réagir au changement d'activité de la ville :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useActivityCity } from "@/hooks/useActivityCity";
|
||||||
|
|
||||||
|
function MyAnimatedObject() {
|
||||||
|
const activityCity = useActivityCity(); // true par défaut, false en mission2
|
||||||
|
|
||||||
|
// L'animation se déclenche quand activityCity change à false
|
||||||
|
// Utiliser useEffect pour réagir au changement
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug
|
||||||
|
|
||||||
|
En mode debug (`?debug` dans l'URL), on peut voir :
|
||||||
|
|
||||||
|
- **Game Step** : L'étape actuelle dans le panneau lil-gui
|
||||||
|
- **Player Position** : Position X, Y, Z du joueur en temps réel
|
||||||
|
- **Zone Visualization** : Anneaux visuels au sol pour les zones + cylindres transparents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes techniques
|
||||||
|
|
||||||
|
- Le mouvement du joueur est bloqué tant que `canMove` est `false`
|
||||||
|
- Le store Zustand (`useGameStore`) est la source principale de vérité
|
||||||
|
- `GameStepManager` synchronise automatiquement avec le store Zustand lors des transitions
|
||||||
|
- Les transitions via les zones utilisent `GameStepManager.transitionTo()` qui met à jour le store
|
||||||
|
- L'audio utilise un callback `onEnded` pour déclencher les transitions automatiques
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# Game States & Substates
|
||||||
|
|
||||||
|
Documentation technique pour le testing et debugging du flow de jeu.
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
```
|
||||||
|
intro ──► bike ──► pylone ──► ferme ──► outro
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main States
|
||||||
|
|
||||||
|
| State | Description |
|
||||||
|
| -------- | ------------------------------------------------------------------ |
|
||||||
|
| `intro` | Séquence d'introduction (cinématique, naming, premier déplacement) |
|
||||||
|
| `bike` | Mission de réparation du vélo |
|
||||||
|
| `pylone` | Quête du pylone (alert → searching → helped → manipulation) |
|
||||||
|
| `ferme` | Mission de réparation de la ferme |
|
||||||
|
| `outro` | Fin du jeu |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intro (GameStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ intro.currentStep │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
intro ──► sequence_video ──► naming ──► start-move ──► bike
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étapes
|
||||||
|
|
||||||
|
| Step | Trigger | Action | Passage vers |
|
||||||
|
| ---------------- | -------- | ---------------------------------------------- | ------------------------------------------------ |
|
||||||
|
| `intro` | Initial | État initial | Auto → `sequence_video` quand `sceneReady: true` |
|
||||||
|
| `sequence_video` | GameFlow | Déclenche la cinématique dans `GameCinematics` | Fin cinématique → `naming` |
|
||||||
|
| `naming` | IntroUI | Affiche l'input pour le prénom | Submit → `start-move` |
|
||||||
|
| `start-move` | GameFlow | Active le mouvement (`canMove: true`) | Via zone "fabrikExit" → `bike` |
|
||||||
|
|
||||||
|
### Transition vers bike
|
||||||
|
|
||||||
|
- **Trigger**: Zone `fabrikExit` dans `zones.ts`
|
||||||
|
- **Action**: `advanceGameState()` dans `ZoneDetection.tsx`
|
||||||
|
- **Résultat**: `mainState: "bike"`, `intro.hasCompleted: true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bike (MissionStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ bike.currentStep (MissionStep) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
locked ──► waiting ──► inspected ──► fragmented ──► scanning ──► repairing ──► reassembling ──► done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transition vers pylone
|
||||||
|
|
||||||
|
- **Trigger**: `bike.currentStep: "done"`
|
||||||
|
- **Action**: `completeBikeState()` dans useGameStore
|
||||||
|
- **Résultat**: `mainState: "pylone"`, `pylone.currentStep: "locked"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pylone (PyloneStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ pylone.currentStep (PyloneStep) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
locked (bypass) ──► alert ──► searching ──► helped ──► manipulation ──► outro
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étapes
|
||||||
|
|
||||||
|
| Step | Trigger | Action | Passage vers |
|
||||||
|
| -------------- | ---------------------------------- | ---------------------------------- | --------------------------------------------------------- |
|
||||||
|
| `locked` | Initial (après bike) | État initial | **Bypass automatique** → `alert` via `advanceGameState()` |
|
||||||
|
| `alert` | `advanceGameState()` | Affiche l'alerte (à implémenter) | Via `advanceGameState()` |
|
||||||
|
| `searching` | `advanceGameState()` | Déclenché par zone "searchingZone" | Via `advanceGameState()` |
|
||||||
|
| `helped` | Interaction avec `NPCHelper` | Dialogue avec le villageois | Via interaction 3D |
|
||||||
|
| `manipulation` | Interaction avec `PyloneDestroyed` | Interaction avec l'objet central | Via `advanceGameState()` → `outro` |
|
||||||
|
|
||||||
|
### Bypass automatique
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// useGameStore.ts - advancePyloneStep()
|
||||||
|
if (state.pylone.currentStep === "locked") {
|
||||||
|
return { pylone: { ...state.pylone, currentStep: "alert" } };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transition vers outro
|
||||||
|
|
||||||
|
- **Trigger**: `pylone.currentStep: "manipulation"` + `advanceGameState()`
|
||||||
|
- **Action**: `advancePyloneStep()` détecte fin de la séquence
|
||||||
|
- **Résultat**: `mainState: "outro"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ferme (MissionStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ferme.currentStep (MissionStep) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
locked ──► waiting ──► inspected ──► fragmented ──► scanning ──► repairing ──► reassembling ──► done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transition vers outro
|
||||||
|
|
||||||
|
- **Trigger**: `ferme.currentStep: "done"`
|
||||||
|
- **Action**: `completeFermeState()` dans useGameStore
|
||||||
|
- **Résultat**: `mainState: "outro"`, `ferme.irrigationFixed: true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Outro
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ outro.hasStarted │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
waiting ──► started
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug Panel
|
||||||
|
|
||||||
|
Le debug panel permet de tester toutes les transitions :
|
||||||
|
|
||||||
|
### Utilisation
|
||||||
|
|
||||||
|
1. Ouvrir le jeu en mode debug (`Debug: true` dans `Debug.ts`)
|
||||||
|
2. Le panneau "Game State" apparaît en bas à gauche
|
||||||
|
3. **Main state**: Sélectionner le state principal
|
||||||
|
4. **Sub state**: Sélectionner le sub-state
|
||||||
|
5. **Previous/Next step**: Avancer ou reculer d'un step
|
||||||
|
6. **Reset**: Remettre à l'état initial
|
||||||
|
|
||||||
|
### Raccourcis clavier
|
||||||
|
|
||||||
|
| Action | Clavier |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| Avancer | Debug panel button |
|
||||||
|
| Reculer | Debug panel button |
|
||||||
|
| Reset | Debug panel button |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comment tester chaque section
|
||||||
|
|
||||||
|
### Tester l'intro
|
||||||
|
|
||||||
|
1. Vérifier que `sceneReady: false` au démarrage
|
||||||
|
2. Attendre que le loader termine (`sceneReady: true`)
|
||||||
|
3. Vérifier `intro.currentStep: "intro"` → auto vers `sequence_video`
|
||||||
|
4. Si cinématique fonctionne : `sequence_video` → `naming`
|
||||||
|
5. Entrer un prénom : `naming` → `start-move`
|
||||||
|
6. Vérifier `canMove: true` après `start-move`
|
||||||
|
7. Entrer dans la zone `fabrikExit` → `mainState: "bike"`
|
||||||
|
|
||||||
|
### Tester bike
|
||||||
|
|
||||||
|
1. Via debug panel, avancer jusqu'à `done`
|
||||||
|
2. Vérifier `mainState: "pylone"`
|
||||||
|
|
||||||
|
### Tester pylone
|
||||||
|
|
||||||
|
1. Via debug panel, avancer (bypass `locked` → `alert`)
|
||||||
|
2. Vérifier `pylone.currentStep: "alert"`
|
||||||
|
3. Avancer : `alert` → `searching` → `helped` → `manipulation`
|
||||||
|
4. Après `manipulation`, vérifier `mainState: "outro"`
|
||||||
|
|
||||||
|
### Tester ferme
|
||||||
|
|
||||||
|
1. Via debug panel, avancer dans bike jusqu'à `done`
|
||||||
|
2. Vérifier `mainState: "pylone"` → `ferme` (après pylone)
|
||||||
|
3. Avancer jusqu'à `done`
|
||||||
|
4. Vérifier `mainState: "outro"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fichiers clés
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
| ------------------------------------------------- | ------------------------------------- |
|
||||||
|
| `src/managers/stores/useGameStore.ts` | Store Zustand avec toutes les actions |
|
||||||
|
| `src/types/game.ts` | Définition de `GameStep` |
|
||||||
|
| `src/types/gameplay/pylone.ts` | Définition de `PyloneStep` |
|
||||||
|
| `src/types/gameplay/repairMission.ts` | Définition de `MissionStep` |
|
||||||
|
| `src/components/game/GameFlow.tsx` | Logique de transition de l'intro |
|
||||||
|
| `src/components/zone/ZoneDetection.tsx` | Déclenchement des zones |
|
||||||
|
| `src/components/ui/debug/GameStateDebugPanel.tsx` | Outil de debug |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## État initial
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
mainState: "intro",
|
||||||
|
isCinematicPlaying: false,
|
||||||
|
sceneReady: false,
|
||||||
|
missionFlow: {
|
||||||
|
activityCity: true,
|
||||||
|
canMove: false,
|
||||||
|
dialogMessage: null,
|
||||||
|
playerName: "",
|
||||||
|
},
|
||||||
|
intro: { currentStep: "intro", dialogueAudio: null, hasCompleted: false, isBikeUnlocked: false },
|
||||||
|
bike: { currentStep: "locked", dialogueAudio: null, isRepaired: false },
|
||||||
|
pylone: { currentStep: "locked", dialogueAudio: null, isPowered: false },
|
||||||
|
ferme: { currentStep: "locked", dialogueAudio: null, irrigationFixed: false },
|
||||||
|
outro: { dialogueAudio: null, hasStarted: false },
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# Hand Tracking Technical Notes
|
||||||
|
|
||||||
|
This document describes the hand tracking system that exists in the current codebase.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Hand tracking started as a debug-stage interaction system used to test direct 3D object manipulation with a webcam. It allows a user to close their fist to grab a nearby object and move it in 3D space without relying on the center crosshair.
|
||||||
|
|
||||||
|
It is now also available to the production repair flow when a mission reaches a hand-driven step.
|
||||||
|
|
||||||
|
## Runtime Flow
|
||||||
|
|
||||||
|
1. The browser captures webcam frames in `src/hooks/handTracking/useRemoteHandTracking.ts`.
|
||||||
|
2. Frames are sent to the local Python backend over WebSocket.
|
||||||
|
3. The backend runs MediaPipe hand landmark detection.
|
||||||
|
4. The backend returns hand data including landmarks, handedness, score, center point, and `isFist`.
|
||||||
|
5. React stores the latest snapshot in the hand tracking provider.
|
||||||
|
6. `GrabbableObject` reads that snapshot each frame and uses fist state plus raycasting to grab objects.
|
||||||
|
7. `HandTrackingGlove` reads the same snapshot and places the rigged `gant_l` and `gant_r` models on the detected hands when hand tracking is active.
|
||||||
|
|
||||||
|
## Activation Rules
|
||||||
|
|
||||||
|
Hand tracking is intentionally gated so the webcam and backend are not used all the time.
|
||||||
|
|
||||||
|
The debug activation conditions are:
|
||||||
|
|
||||||
|
- debug mode is active with `?debug`
|
||||||
|
- scene mode is `physics`
|
||||||
|
- the player is near an interaction, is holding an object, or is hand-holding an object
|
||||||
|
|
||||||
|
This keeps hand tracking active while the player is inside an interaction zone, even if the camera is not aimed directly at the object.
|
||||||
|
|
||||||
|
The production repair activation conditions are:
|
||||||
|
|
||||||
|
- active `mainState` is `bike`, `pylone`, or `ferme`
|
||||||
|
- the active mission step is `inspected`, `repairing`, `reassembling`, or `done`
|
||||||
|
|
||||||
|
This keeps the webcam off during `waiting`, `fragmented`, and `scanning`, then enables hand input only when the repair flow is expected to use hands.
|
||||||
|
|
||||||
|
In the current production repair flow, `inspected` uses a two-fists hold gesture to advance to `fragmented`. The hold must last one second and is independent from local object interaction distance once the mission is in the correct state. Keyboard input for the same transition is handled separately by the repair case trigger, so pressing `E` requires the case to be focused through the shared interaction system.
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
The backend lives in `backend/` and exposes:
|
||||||
|
|
||||||
|
- `GET /health` for health checks
|
||||||
|
- `WS /ws` for frame input and hand tracking output
|
||||||
|
|
||||||
|
The Python process uses MediaPipe and the local model file:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
backend/hand_landmarker.task
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend sends normalized hand coordinates and landmarks. The frontend treats the values as screen-space inputs, then maps them into world space with the active Three.js camera.
|
||||||
|
|
||||||
|
## Frontend Data Shape
|
||||||
|
|
||||||
|
The shared types live in `src/types/handTracking/handTracking.ts`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface HandTrackingHand {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
landmarks: HandTrackingLandmark[];
|
||||||
|
handedness: string;
|
||||||
|
isFist: boolean;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`x` and `y` are normalized camera coordinates. `z` is a relative depth value from MediaPipe, not an absolute world-space distance.
|
||||||
|
|
||||||
|
## Grab Targeting
|
||||||
|
|
||||||
|
The hand grab logic lives in `src/components/three/interaction/GrabbableObject.tsx`.
|
||||||
|
|
||||||
|
The object is moved toward the visual center of the hand. That center is computed from the bounding box of all landmarks:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
centerX = (minX + maxX) / 2
|
||||||
|
centerY = (minY + maxY) / 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Starting a grab uses a slightly wider virtual hit zone. Instead of raycasting only from one point, the code casts several rays around the hand center:
|
||||||
|
|
||||||
|
- center
|
||||||
|
- left
|
||||||
|
- right
|
||||||
|
- up
|
||||||
|
- down
|
||||||
|
|
||||||
|
If any ray hits the object while the object is within `INTERACTION_RADIUS`, the object enters hand-holding mode.
|
||||||
|
|
||||||
|
## Depth Handling
|
||||||
|
|
||||||
|
Because MediaPipe `z` is relative and noisy, the current frontend does not use it as a direct world-depth controller for object grabbing.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
This is less expressive than true depth-aware hand movement, but it is more stable for the current first-person prototype.
|
||||||
|
|
||||||
|
## UI And Debug
|
||||||
|
|
||||||
|
The current debug UI includes:
|
||||||
|
|
||||||
|
- `HandTrackingDebugPanel` inside `DebugOverlayLayout` for status, usage, loaded glove model, server state, hand count, and fist state
|
||||||
|
- `HandTrackingVisualizer` for the SVG landmark wireframe fallback
|
||||||
|
- `HandTrackingGlove` for the left-hand `gant_l` and right-hand `gant_r` models in the R3F scene
|
||||||
|
- `r3f-perf` for render performance
|
||||||
|
- `lil-gui` for scene, camera, lighting, interaction, and grab controls
|
||||||
|
|
||||||
|
The hand tracking debug panel is a compact HTML grid outside the canvas. `Model loaded` displays the successfully loaded glove models. The SVG hand wireframe is only a fallback while models are loading or if a glove model fails to load.
|
||||||
|
|
||||||
|
## Glove Models
|
||||||
|
|
||||||
|
The current glove MVP uses `public/models/gant_l/model.gltf` and `public/models/gant_r/model.gltf`, which contain GLTF skins and armatures. Each model is positioned, oriented, and scaled from palm landmarks, then each finger bone chain is rotated toward the matching MediaPipe landmark chain.
|
||||||
|
|
||||||
|
The glove models are intentionally smaller than the raw SVG overlay so they do not dominate the camera view.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- Production usage is currently limited to repair mission steps that explicitly need hands.
|
||||||
|
- 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.
|
||||||
|
- Finger bone animation is an approximate landmark-to-bone mapping; it still needs calibration for per-model twist, offsets, and smoothing.
|
||||||
@@ -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,79 @@
|
|||||||
|
# Mission Flow
|
||||||
|
|
||||||
|
This document describes the mission intro and mission 2 prototype flow after it was merged into the current architecture.
|
||||||
|
|
||||||
|
## Source Of Truth
|
||||||
|
|
||||||
|
Mission flow state lives in the global game store:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/managers/stores/useGameStore.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
The store owns the `missionFlow` slice:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
missionFlow: {
|
||||||
|
step: GameStep;
|
||||||
|
activityCity: boolean;
|
||||||
|
playerName: string;
|
||||||
|
canMove: boolean;
|
||||||
|
dialogMessage: string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps global gameplay state in Zustand instead of splitting it across a separate mission store or a gameplay manager.
|
||||||
|
|
||||||
|
## Managers Boundary
|
||||||
|
|
||||||
|
Managers stay responsible for local runtime services:
|
||||||
|
|
||||||
|
- `AudioManager` owns audio elements, audio pools, music playback, category volume, and stereo pan.
|
||||||
|
- `InteractionManager` owns transient focused/nearby/held interaction handles.
|
||||||
|
|
||||||
|
Mission progression is not owned by a manager. Components update the store through explicit actions such as `setFlowStep`, `setCanMove`, `showDialog`, and `hideDialog`.
|
||||||
|
|
||||||
|
## Runtime Components
|
||||||
|
|
||||||
|
- `src/components/game/GameFlow.tsx` reacts to `missionFlow.step` and triggers one-off side effects such as intro audio and movement unlocks.
|
||||||
|
- `src/components/zone/ZoneDetection.tsx` reads the camera position and moves the flow to a target step when the player enters a configured zone.
|
||||||
|
- `src/components/three/interaction/CentralObject.tsx` and `VillageoisHelperObject.tsx` expose temporary interactive mission objects.
|
||||||
|
- `src/pages/page.tsx` mounts mission HTML overlays: `IntroUI`, `BienvenueDisplay`, and `DialogMessage`.
|
||||||
|
- `src/world/player/PlayerController.tsx` reads `missionFlow.canMove` as an additional movement lock.
|
||||||
|
|
||||||
|
## Step Sequence
|
||||||
|
|
||||||
|
The prototype currently uses these steps:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
"intro" |
|
||||||
|
"start-intro" |
|
||||||
|
"naming" |
|
||||||
|
"bienvenue" |
|
||||||
|
"star-move" |
|
||||||
|
"mission2" |
|
||||||
|
"searching" |
|
||||||
|
"helped" |
|
||||||
|
"manipulation" |
|
||||||
|
"outOfFabrik";
|
||||||
|
```
|
||||||
|
|
||||||
|
These steps are mission-flow prototype states. They do not replace `mainState` or the repair mission step machine used by `RepairGame`.
|
||||||
|
|
||||||
|
## Zone Configuration
|
||||||
|
|
||||||
|
Zone triggers live in:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/data/zones.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Each zone has an id, position, radius, height, and `targetStep`. `ZoneDetection` marks a zone as triggered after the first activation so the same zone does not replay its transition every frame.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Keep mission flow state in `useGameStore.missionFlow`.
|
||||||
|
- Do not reintroduce `GameStepManager` for global state transitions.
|
||||||
|
- Do not create a second Zustand store for mission flow unless the state becomes independent from game progression.
|
||||||
|
- Keep side effects such as audio playback in components or service managers, but keep the state transition itself in the store.
|
||||||
|
- Keep per-frame values such as camera position and zone distance checks out of Zustand.
|
||||||
@@ -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.
|
||||||
@@ -5,7 +5,7 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
## Relationship To The Current Code
|
## Relationship To The Current Code
|
||||||
|
|
||||||
- `docs/technical/architecture.md` is the source of truth for what exists now.
|
- `docs/technical/architecture.md` is the source of truth for what exists now.
|
||||||
- This document is intentionally aspirational.
|
- This document describes intended direction, not implemented behavior.
|
||||||
- If this document conflicts with the current implementation, the current implementation wins.
|
- If this document conflicts with the current implementation, the current implementation wins.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
@@ -40,12 +40,12 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
- performance overlay
|
- performance overlay
|
||||||
- scene helpers
|
- scene helpers
|
||||||
- free camera and calibration controls
|
- free camera and calibration controls
|
||||||
- temporary test scenes used during development
|
- debug test scenes used during development
|
||||||
|
|
||||||
### UI Layer
|
### UI Layer
|
||||||
|
|
||||||
- `src/components/ui/` should contain player-facing HTML overlays.
|
- `src/components/ui/` should contain player-facing HTML overlays.
|
||||||
- Expected future examples:
|
- Candidate examples:
|
||||||
- crosshair
|
- crosshair
|
||||||
- loading flow
|
- loading flow
|
||||||
- mission HUD
|
- mission HUD
|
||||||
@@ -54,7 +54,7 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
### Gameplay Layer
|
### Gameplay Layer
|
||||||
|
|
||||||
- As the project grows, gameplay state can move toward a clearer orchestration layer.
|
- As the project grows, gameplay state can move toward a clearer orchestration layer.
|
||||||
- Likely future concerns:
|
- Likely concerns:
|
||||||
- missions
|
- missions
|
||||||
- zones
|
- zones
|
||||||
- cinematics
|
- cinematics
|
||||||
@@ -67,4 +67,4 @@ This document describes the intended medium-term architecture for the project.
|
|||||||
- Prefer direct, working code over speculative scaffolding.
|
- Prefer direct, working code over speculative scaffolding.
|
||||||
- Shared types should stay close to their domain until they have multiple real consumers.
|
- Shared types should stay close to their domain until they have multiple real consumers.
|
||||||
- Avoid creating new managers or service layers without an active runtime need.
|
- Avoid creating new managers or service layers without an active runtime need.
|
||||||
- Debug-only runtime paths should be clearly marked and easy to remove later.
|
- Debug-only runtime paths should be clearly marked and easy to remove when obsolete.
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
# Zustand Stores
|
||||||
|
|
||||||
|
This document explains how Zustand is used in the current project.
|
||||||
|
|
||||||
|
## Why Zustand Exists Here
|
||||||
|
|
||||||
|
The project needs shared state that is durable enough to be read by multiple React and React Three Fiber systems.
|
||||||
|
|
||||||
|
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 pylon repair sequence |
|
||||||
|
| `ferme` | Vertical farm repair sequence |
|
||||||
|
| `outro` | Ending sequence |
|
||||||
|
|
||||||
|
Other important state:
|
||||||
|
|
||||||
|
- `isCinematicPlaying`
|
||||||
|
- `intro`
|
||||||
|
- `bike`
|
||||||
|
- `pylone`
|
||||||
|
- `ferme`
|
||||||
|
- `outro`
|
||||||
|
|
||||||
|
Mission steps:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
"locked" |
|
||||||
|
"waiting" |
|
||||||
|
"inspected" |
|
||||||
|
"fragmented" |
|
||||||
|
"scanning" |
|
||||||
|
"repairing" |
|
||||||
|
"reassembling" |
|
||||||
|
"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.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
|
|
||||||
|
export function Example(): React.JSX.Element {
|
||||||
|
const mainState = useGameStore((state) => state.mainState);
|
||||||
|
|
||||||
|
return <p>Current state: {mainState}</p>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is better than reading the whole store, because the component re-renders only when `mainState` changes.
|
||||||
|
|
||||||
|
## Updating Game State
|
||||||
|
|
||||||
|
Prefer explicit actions from the store.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const advanceGameState = useGameStore((state) => state.advanceGameState);
|
||||||
|
|
||||||
|
advanceGameState();
|
||||||
|
```
|
||||||
|
|
||||||
|
For development and debug tooling, direct setters also exist:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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`
|
||||||
|
- `completePylone`
|
||||||
|
- `completeFerme`
|
||||||
|
- `completeMission`
|
||||||
|
|
||||||
|
Mission gameplay that can target `bike`, `pylone`, or `ferme` should prefer generic mission actions:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const setMissionStep = useGameStore((state) => state.setMissionStep);
|
||||||
|
const completeMission = useGameStore((state) => state.completeMission);
|
||||||
|
|
||||||
|
setMissionStep("bike", "inspected");
|
||||||
|
completeMission("bike");
|
||||||
|
```
|
||||||
|
|
||||||
|
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 the repair-game content.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/types/gameplay/repairMission.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
`src/components/ui/GameUI.tsx` groups the HTML overlays used by the playable route.
|
||||||
|
|
||||||
|
Current overlays:
|
||||||
|
|
||||||
|
- `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`: indicator intended for repair movement lock
|
||||||
|
- `HandTrackingVisualizer`: hand tracking SVG fallback/debug visualization
|
||||||
|
- `Subtitles`: active dialogue subtitle overlay
|
||||||
|
- `GameSettingsMenu`: options menu and settings controls
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- Do not store per-frame values in Zustand.
|
||||||
|
- Use `useRef` for high-frequency mutable values such as player velocity, temporary vectors, or animation-loop data.
|
||||||
|
- Use selectors instead of reading the whole store in components.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- 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.
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
# Editor User Guide
|
||||||
|
|
||||||
|
The map editor is available at `/editor`. It is a browser-based tool for editing the runtime map, cinematic manifest, dialogue manifest, and SRT subtitle files without manually jumping between JSON and subtitle files.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Use the editor when you need to:
|
||||||
|
|
||||||
|
- move, rotate, or scale objects from `public/map.json`
|
||||||
|
- inspect the raw JSON generated by the editor
|
||||||
|
- preview and edit cinematics from `public/cinematics.json`
|
||||||
|
- create, preview, and validate dialogue entries from `public/sounds/dialogue/dialogues.json`
|
||||||
|
- edit FR/EN SRT subtitle files per voice
|
||||||
|
|
||||||
|
The map editor reads the same map data as the runtime scene:
|
||||||
|
|
||||||
|
- `public/map.json` contains the object list.
|
||||||
|
- `public/models/{name}/model.glb` contains the matching 3D model for each object name. `model.gltf` is still supported as a fallback during migration.
|
||||||
|
- Missing models are displayed as gray fallback cubes, so incomplete maps remain editable.
|
||||||
|
|
||||||
|
## Map Node Format
|
||||||
|
|
||||||
|
Each entry in `public/map.json` represents one object:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| ---------- | ------------------------------------------------- |
|
||||||
|
| `name` | Model folder name in `public/models/{name}` |
|
||||||
|
| `type` | Object category |
|
||||||
|
| `position` | Object position as `[x, y, z]` |
|
||||||
|
| `rotation` | Object rotation as `[x, y, z]`, expressed radians |
|
||||||
|
| `scale` | Object scale as `[x, y, z]` |
|
||||||
|
|
||||||
|
## Panel Layout
|
||||||
|
|
||||||
|
The right panel is split into dropdown groups:
|
||||||
|
|
||||||
|
- `Editor`: map transform tools, shortcuts, selection, view mode, JSON preview, and file actions.
|
||||||
|
- `Cinematics`: editor for `public/cinematics.json`.
|
||||||
|
- `Dialogues`: editor for `public/sounds/dialogue/dialogues.json`.
|
||||||
|
- `SRT`: editor for subtitle files in `public/sounds/dialogue/subtitles/`.
|
||||||
|
|
||||||
|
Only the `Editor` group is open by default. Open the other groups when you need audio or cinematic tooling.
|
||||||
|
|
||||||
|
## Map Editing Workflow
|
||||||
|
|
||||||
|
1. Open `/editor` in the local app.
|
||||||
|
2. Click an object in the scene to select it.
|
||||||
|
3. Choose a transform mode: translate, rotate, or scale.
|
||||||
|
4. Drag the transform gizmo in the 3D view.
|
||||||
|
5. Check the JSON inspector if you need exact values.
|
||||||
|
6. Use undo or redo if the transform is not correct.
|
||||||
|
7. Export the JSON or save it to the dev server.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
| Action | Input |
|
||||||
|
| -------------------- | -------------------------- |
|
||||||
|
| Select object | Click object |
|
||||||
|
| Deselect | `Esc` or click empty space |
|
||||||
|
| Lock selection | `Lock` button in Selection |
|
||||||
|
| Clear selection | `X` button in Selection |
|
||||||
|
| Translate mode | `T` |
|
||||||
|
| Rotate mode | `R` |
|
||||||
|
| Scale mode | `S` |
|
||||||
|
| Undo | `Ctrl+Z` |
|
||||||
|
| Redo | `Ctrl+Y` |
|
||||||
|
| Locked view movement | `WASD`, `ZQSD`, arrows |
|
||||||
|
| Move up | `Space` |
|
||||||
|
| Move down | `Shift` |
|
||||||
|
|
||||||
|
## Selection
|
||||||
|
|
||||||
|
The `Selection` section shows the selected object name and its index in `public/map.json`.
|
||||||
|
|
||||||
|
- Click an object to select it.
|
||||||
|
- Click empty space or press `Esc` to clear the selection.
|
||||||
|
- Use the `X` button to clear the selection explicitly.
|
||||||
|
- Use the `Lock` button to protect the current selection while editing.
|
||||||
|
|
||||||
|
When selection is locked:
|
||||||
|
|
||||||
|
- clicking another object does not change the selection
|
||||||
|
- clicking empty space does not clear the selection
|
||||||
|
- pressing `Esc` does not clear the selection
|
||||||
|
- the `X` button still clears the selection intentionally
|
||||||
|
|
||||||
|
## View Mode
|
||||||
|
|
||||||
|
The `Lock view` action switches the editor into a movement mode closer to the runtime player camera. Use it to navigate larger scenes while keeping the transform tools available.
|
||||||
|
|
||||||
|
## JSON Inspector
|
||||||
|
|
||||||
|
The `JSON` section shows the raw map data that will be exported or saved:
|
||||||
|
|
||||||
|
- When no object is selected, it shows the full map node list.
|
||||||
|
- When an object is selected, it highlights the JSON lines for that object.
|
||||||
|
|
||||||
|
Use it to verify exact numeric transform values before saving or exporting. The JSON inspector is read-only; transform values are changed through the gizmo in the scene.
|
||||||
|
|
||||||
|
## Saving Changes
|
||||||
|
|
||||||
|
### Export JSON
|
||||||
|
|
||||||
|
`Export JSON` downloads the current map node list as `map.json`. Use this when you want to manually replace `public/map.json`.
|
||||||
|
|
||||||
|
### Save To Server
|
||||||
|
|
||||||
|
`Save to server` is available only during local development. It writes the edited map back to `public/map.json` through the Vite dev-server endpoint.
|
||||||
|
|
||||||
|
The button is hidden in production builds because production persistence is not implemented.
|
||||||
|
|
||||||
|
## Editing Dialogue Subtitles
|
||||||
|
|
||||||
|
The side panel includes two separate audio text tools:
|
||||||
|
|
||||||
|
- `Dialogues` edits the dialogue manifest, which links dialogue IDs to audio files and SRT cue indexes.
|
||||||
|
- `SRT` edits the actual subtitle text and cue timings.
|
||||||
|
|
||||||
|
The important model is: one dialogue entry points to one cue inside one SRT file. The SRT file is grouped by voice and language, not by dialogue.
|
||||||
|
|
||||||
|
### Dialogue Manifest
|
||||||
|
|
||||||
|
Use the `Dialogues` panel to edit `public/sounds/dialogue/dialogues.json` without opening the JSON file manually.
|
||||||
|
|
||||||
|
Each dialogue entry contains:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| ------------------ | ----------------------------------------------------------------- |
|
||||||
|
| `id` | Unique dialogue ID used by cinematics and runtime triggers |
|
||||||
|
| `voice` | Voice file group: `narrateur`, `fermier`, or `electricienne` |
|
||||||
|
| `audio` | Runtime audio path, usually under `/sounds/dialogue/` |
|
||||||
|
| `subtitleCueIndex` | Cue number inside the selected voice/language SRT file |
|
||||||
|
| `timecode` | Optional global runtime trigger time, in seconds from scene start |
|
||||||
|
|
||||||
|
Available actions:
|
||||||
|
|
||||||
|
- `Reload` reloads the manifest from disk.
|
||||||
|
- `Add` creates a local dialogue entry for the current voice and assigns the next available SRT cue index.
|
||||||
|
- `Save` writes the manifest through the local Vite dev server.
|
||||||
|
- `Preview dialogue` plays the selected dialogue and shows subtitles in the editor overlay.
|
||||||
|
- `Create FR SRT cue` creates the matching French SRT cue if it is missing.
|
||||||
|
- `Delete dialogue` removes the selected entry locally.
|
||||||
|
|
||||||
|
After using `Add`, save the manifest to keep the new dialogue entry. The generated SRT cue is written immediately to the French SRT file, but the dialogue manifest is still only local until `Save` is clicked.
|
||||||
|
|
||||||
|
New dialogue audio paths start as placeholders such as `/sounds/dialogue/new_dialogue_24.mp3`. Replace them with real MP3 paths before validating the final asset set.
|
||||||
|
|
||||||
|
Recommended workflow for a new dialogue:
|
||||||
|
|
||||||
|
1. Open `Dialogues`.
|
||||||
|
2. Click `Add`.
|
||||||
|
3. Choose the correct `voice`.
|
||||||
|
4. Replace the generated `id` with a readable stable ID.
|
||||||
|
5. Replace the placeholder `audio` path with the real MP3 path.
|
||||||
|
6. Check the generated `subtitleCueIndex`.
|
||||||
|
7. Click `Create FR SRT cue` if the cue does not exist yet.
|
||||||
|
8. Click `Save`.
|
||||||
|
9. Open `SRT`, edit the cue text and timings, then save the SRT file.
|
||||||
|
10. Run `Validate` from the SRT panel.
|
||||||
|
|
||||||
|
### SRT Editor
|
||||||
|
|
||||||
|
Use the `SRT` panel to edit one subtitle file at a time.
|
||||||
|
|
||||||
|
1. Choose a voice: `narrateur`, `fermier`, or `electricienne`.
|
||||||
|
2. Choose a language: `FR` or `EN`.
|
||||||
|
3. Edit the SRT text directly in the textarea.
|
||||||
|
4. Use the audio preview to check the selected dialogue.
|
||||||
|
5. Use `Set start`, `Set end`, `-100ms`, and `+100ms` to adjust the selected cue timing against the audio.
|
||||||
|
6. Use `Save SRT` during local development, or `Export SRT` to download the file manually.
|
||||||
|
|
||||||
|
Each SRT file belongs to one voice, not one dialogue. Cue indexes must match the `subtitleCueIndex` values referenced by the dialogue manifest.
|
||||||
|
|
||||||
|
SRT timings are relative to the dialogue audio file, not to the global game timeline and not to the cinematic timeline. For example, `00:00:01,000` means one second after that dialogue audio starts.
|
||||||
|
|
||||||
|
## Validating Dialogue Assets
|
||||||
|
|
||||||
|
Use `Validate` in the SRT panel to check the dialogue manifest and linked assets.
|
||||||
|
|
||||||
|
The validation checks:
|
||||||
|
|
||||||
|
- `public/sounds/dialogue/dialogues.json`
|
||||||
|
- referenced dialogue audio files
|
||||||
|
- French SRT files
|
||||||
|
- subtitle cue indexes referenced by the manifest
|
||||||
|
|
||||||
|
Missing English SRT files are warnings, not errors, because the runtime falls back to French subtitles. This is intentional until the English translation workflow is ready.
|
||||||
|
|
||||||
|
## Editing Cinematics
|
||||||
|
|
||||||
|
Use the `Cinematics` panel to edit `public/cinematics.json`.
|
||||||
|
|
||||||
|
Each cinematic contains:
|
||||||
|
|
||||||
|
- an `id`
|
||||||
|
- an optional global `timecode`
|
||||||
|
- two or more camera keyframes
|
||||||
|
- optional dialogue cues synchronized to the cinematic timeline
|
||||||
|
|
||||||
|
Camera keyframes define:
|
||||||
|
|
||||||
|
- `time`: seconds relative to the cinematic start
|
||||||
|
- `position`: camera position `[x, y, z]`
|
||||||
|
- `target`: point the camera looks at `[x, y, z]`
|
||||||
|
|
||||||
|
Dialogue cues define:
|
||||||
|
|
||||||
|
- `time`: seconds relative to the cinematic start
|
||||||
|
- `dialogueId`: an entry from `public/sounds/dialogue/dialogues.json`
|
||||||
|
|
||||||
|
Recommended workflow for a cinematic:
|
||||||
|
|
||||||
|
1. Open `Cinematics`.
|
||||||
|
2. Select an existing cinematic or click `Add`.
|
||||||
|
3. Set a stable `id`.
|
||||||
|
4. Add or adjust camera keyframes.
|
||||||
|
5. Keep keyframe `time` values increasing from start to end.
|
||||||
|
6. Add dialogue cues when a dialogue must start during the camera sequence.
|
||||||
|
7. Click `Preview cinematic` to test the camera path in the editor canvas.
|
||||||
|
8. Click `Save` when the manifest is correct.
|
||||||
|
|
||||||
|
Available actions:
|
||||||
|
|
||||||
|
- `Reload` reloads the cinematic manifest from disk.
|
||||||
|
- `Add` creates a new local cinematic with two camera keyframes.
|
||||||
|
- `Save` writes `public/cinematics.json` through the local Vite dev server.
|
||||||
|
- `Preview cinematic` plays the selected camera animation in the editor canvas.
|
||||||
|
- `Add keyframe` and `Remove` edit the camera path.
|
||||||
|
- `Add dialogue` and `Remove` edit dialogue cues linked to the cinematic.
|
||||||
|
- `Delete cinematic` removes the selected cinematic locally.
|
||||||
|
|
||||||
|
Cinematic dialogue cues are the preferred way to synchronize a dialogue with a cinematic. Avoid also giving the same dialogue a global `timecode`, or it can be triggered twice.
|
||||||
|
|
||||||
|
Use `dialogueCues` when the dialogue belongs to a cinematic. Use a dialogue `timecode` only for simple global scene timing outside a cinematic.
|
||||||
|
|
||||||
|
## Current Limitations
|
||||||
|
|
||||||
|
- The editor only modifies existing nodes.
|
||||||
|
- It does not create or delete objects.
|
||||||
|
- It does not edit model files or textures.
|
||||||
|
- It does not provide production persistence.
|
||||||
|
- Fallback cubes indicate missing models; they are editor placeholders, not exported assets.
|
||||||
|
- SRT saving is a local Vite dev-server helper, not a production backend feature.
|
||||||
|
- Dialogue and cinematic saves are local Vite dev-server helpers, not production backend features.
|
||||||
+226
-31
@@ -1,49 +1,244 @@
|
|||||||
# Implemented Features
|
# Implemented Features
|
||||||
|
|
||||||
This document lists features that are implemented in the current codebase.
|
This document lists the user-visible and developer-facing features implemented in the current `develop` branch.
|
||||||
|
|
||||||
## Scene
|
## Application And Routes
|
||||||
|
|
||||||
- Fullscreen React Three Fiber scene
|
- React 19 application bootstrapped by Vite and TypeScript
|
||||||
- Main map scene loaded from `public/models/map/model.gltf`
|
- TanStack Router route tree
|
||||||
- Debug physics test scene selectable from the debug panel
|
- `/` playable 3D experience
|
||||||
- Ambient and directional lighting
|
- `/editor` local content editor
|
||||||
- Environment background setup
|
- `/docs` in-app documentation browser
|
||||||
|
- Lazy-loaded docs pages rendered from repository Markdown files
|
||||||
|
|
||||||
|
## 3D World
|
||||||
|
|
||||||
|
- Fullscreen React Three Fiber canvas
|
||||||
|
- Production world composition in `src/world/World.tsx`
|
||||||
|
- Environment model/background through `Environment` and `SkyModel`
|
||||||
|
- Shared lighting setup
|
||||||
|
- Production map loaded from `public/map.json`
|
||||||
|
- Model resolution from `public/models/{name}/model.glb`, then `model.gltf`
|
||||||
|
- Fallback cubes when a map node has no available model
|
||||||
|
- Progressive scene loading overlay for game, debug physics scene, and editor
|
||||||
|
- Stabilized game scene loading gates for map, model, collision, octree, and gameplay stage readiness
|
||||||
|
- Game stage content mounted only after the map has loaded
|
||||||
|
- Player, music, dialogues, and gameplay-dependent systems mounted only after gameplay is ready
|
||||||
|
|
||||||
## Player
|
## Player
|
||||||
|
|
||||||
- Player camera mode
|
- Player camera mode
|
||||||
- Pointer lock mouse look
|
- Pointer-lock mouse look
|
||||||
- Movement with `ZQSD`
|
- `ZQSD` movement
|
||||||
- Jumping
|
- Jump with `Space`
|
||||||
- Octree-based collision against the loaded map
|
- Trigger interaction with `E`
|
||||||
|
- Grab interaction with primary mouse button
|
||||||
|
- Spawn reset based on scene mode
|
||||||
|
- Input lock while the settings menu is open
|
||||||
|
- Input lock while a cinematic is playing
|
||||||
|
- Octree collision against dedicated map collision nodes, currently scoped to the `terrain` node
|
||||||
|
- Repair movement-lock hook and indicator exist, but the hook currently returns `false`, so movement is not locked during repair on the current branch
|
||||||
|
|
||||||
## Interactions
|
## Physics And Collision
|
||||||
|
|
||||||
- Focus detection by distance and raycast
|
- Separate collision responsibility between player and gameplay objects
|
||||||
- Trigger interactions activated with `E`
|
- Player collision uses a Three.js capsule plus octree
|
||||||
- Grab interactions activated with the primary mouse button
|
- Gameplay objects use Rapier rigid bodies and colliders
|
||||||
- Interaction prompt shown for trigger interactions
|
- Production `GameStageContent` is mounted inside a Rapier `Physics` provider
|
||||||
|
- Debug physics scene owns its own Rapier playground
|
||||||
|
- Map collision octree is built from explicit collision nodes instead of the full visible map
|
||||||
|
|
||||||
|
## Interaction System
|
||||||
|
|
||||||
|
- Shared `InteractionManager` singleton for focused object, nearby object, holding state, and hand-holding state
|
||||||
|
- React subscription through `useSyncExternalStore`
|
||||||
|
- Distance and camera-ray focus detection in `InteractableObject`
|
||||||
|
- Trigger interactions through `TriggerObject`
|
||||||
|
- Grab interactions through `GrabbableObject`
|
||||||
|
- Trigger prompt shown by `InteractPrompt`
|
||||||
|
- Optional trigger SFX and optional spawned model support
|
||||||
|
- Debug interaction sphere visibility through the `Interaction` lil-gui folder
|
||||||
|
- Hand-controlled grab support for grabbable objects
|
||||||
|
- Snap-to-target behavior after releasing grabbable objects
|
||||||
|
|
||||||
|
## Repair Gameplay
|
||||||
|
|
||||||
|
- Reusable `RepairGame` mounted for `bike`, `pylone`, and `ferme`
|
||||||
|
- Mission progression driven by Zustand and shared `MissionStep` types
|
||||||
|
- Production repair positions:
|
||||||
|
- `bike` at `[8, 0, -6]`
|
||||||
|
- `pylone` at `[64, 0, -66]`
|
||||||
|
- `ferme` at `[-24, 0, 42]`
|
||||||
|
- Debug physics repair playground zones for all three missions
|
||||||
|
- Data-driven mission config in `src/data/gameplay/repairMissions.ts`
|
||||||
|
- Mission flow: `locked -> waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done`
|
||||||
|
- `.webm` 3D prompts for mission object, interaction, and broken parts
|
||||||
|
- Repair object inspection
|
||||||
|
- Repair case spawn, pop animation, proximity float, wobble, open/close lid animation, exit animation, and open/close sounds
|
||||||
|
- Repair case placeholder traversal from GLTF nodes named `placeholder_*`
|
||||||
|
- Fallback placeholder positions when a case asset has no placeholder nodes
|
||||||
|
- Fragmentation through repair-case trigger or two-fists hand gesture
|
||||||
|
- Exploded model visualization through `ExplodableModel`
|
||||||
|
- Scan visual that steps through exploded parts
|
||||||
|
- Broken-part detection by configured `nodeName`, with fallback to first scanned parts
|
||||||
|
- Persistent broken-part highlight and broken-part prompt after discovery
|
||||||
|
- Grabbable replacement part choices, including decoys
|
||||||
|
- Grabbable broken parts that must be deposited back into the case
|
||||||
|
- Snap-to-placeholder placement
|
||||||
|
- Correct-part, wrong-part, and stored-part visual feedback
|
||||||
|
- Blocked install feedback when validation is attempted too early
|
||||||
|
- Install target that validates only when the correct replacement is placed and all broken parts are stored
|
||||||
|
- Inverse reassembly animation
|
||||||
|
- Completion particles
|
||||||
|
- Completion target that closes/exits the repair case before calling `completeMission`
|
||||||
|
|
||||||
|
## Game Progression Store
|
||||||
|
|
||||||
|
- Zustand `useGameStore` for durable gameplay progression
|
||||||
|
- Main states: `intro`, `bike`, `pylone`, `ferme`, `outro`
|
||||||
|
- Per-mission repair step state
|
||||||
|
- Per-mission completion flags
|
||||||
|
- Generic mission helpers: `setMissionStep`, `completeMission`, `advanceGameState`, `rewindGameState`, `resetGame`
|
||||||
|
- `isCinematicPlaying` flag used by the player input lock
|
||||||
|
- Debug game-state panel that can jump between main states and sub-states
|
||||||
|
|
||||||
|
## Settings And UI Overlays
|
||||||
|
|
||||||
|
- `Esc` opens and closes the settings menu
|
||||||
|
- Music, SFX, and dialogue volume sliders
|
||||||
|
- Subtitle visibility toggle
|
||||||
|
- Subtitle language choice between French and English
|
||||||
|
- Repair-runtime choice between JavaScript and Python modes stored in settings
|
||||||
|
- Quit action that clears browser-accessible cookies and returns to `/`
|
||||||
|
- Crosshair overlay
|
||||||
|
- Interaction prompt
|
||||||
|
- Subtitle overlay
|
||||||
|
- Repair movement-lock indicator component, currently inactive because the lock hook returns `false`
|
||||||
|
- Debug overlay layout
|
||||||
|
- Scene loading overlay
|
||||||
|
|
||||||
## Audio
|
## Audio
|
||||||
|
|
||||||
- One-shot sound playback for trigger interactions
|
- Singleton `AudioManager`
|
||||||
- Simple per-sound pooling through `AudioManager`
|
- Looped music playback
|
||||||
|
- One-shot SFX/dialogue playback
|
||||||
|
- Per-path one-shot audio pools
|
||||||
|
- Category volumes for `music`, `sfx`, and `dialogue`
|
||||||
|
- Optional stereo panning for one-shot sounds
|
||||||
|
- Playback-rate option for one-shot sounds
|
||||||
|
- Browser autoplay fallback for music: retry after user `pointerdown` or `keydown`
|
||||||
|
- Game music mounted through `GameMusic`
|
||||||
|
- Current game music path: `/sounds/musique/test.mp3`
|
||||||
|
- Current base music volume: `0.33`
|
||||||
|
- Repair case open/close sounds
|
||||||
|
- Trigger-object SFX support
|
||||||
|
|
||||||
|
## Dialogue And Subtitles
|
||||||
|
|
||||||
|
- Runtime dialogue manifest in `public/sounds/dialogue/dialogues.json`
|
||||||
|
- Dialogue audio under `public/sounds/dialogue/`
|
||||||
|
- One SRT file per voice and language
|
||||||
|
- French and English subtitle folders
|
||||||
|
- Runtime SRT parsing
|
||||||
|
- Subtitle cue lookup by voice, selected language, and `subtitleCueIndex`
|
||||||
|
- French fallback when the selected language file is unavailable
|
||||||
|
- Dialogue playback through the `dialogue` audio category
|
||||||
|
- Runtime subtitle synchronization from audio `timeupdate`
|
||||||
|
- Speaker-aware subtitle overlay
|
||||||
|
- Dialogue queueing to avoid overlapping dialogue playback
|
||||||
|
- Global timecode dialogue triggering through `GameDialogues`
|
||||||
|
|
||||||
|
## Cinematics
|
||||||
|
|
||||||
|
- Runtime cinematic manifest in `public/cinematics.json`
|
||||||
|
- Cinematic manifest validation
|
||||||
|
- GSAP camera keyframe playback
|
||||||
|
- Camera position and look target interpolation
|
||||||
|
- Optional dialogue cues relative to cinematic start time
|
||||||
|
- Player input lock while a cinematic is active
|
||||||
|
- Current world integration only mounts `GameCinematics` during `mainState === "outro"`
|
||||||
|
|
||||||
|
## Hand Tracking
|
||||||
|
|
||||||
|
- Optional webcam hand tracking provider around the playable scene
|
||||||
|
- Source switch in debug GUI: local Python backend or browser-side MediaPipe
|
||||||
|
- Backend WebSocket endpoint at `ws://localhost:8000/ws`
|
||||||
|
- Backend health endpoint at `http://localhost:8000/health`
|
||||||
|
- Browser-side MediaPipe through `@mediapipe/tasks-vision`
|
||||||
|
- Lazy activation so camera/tracking is not always active
|
||||||
|
- Production activation during repair steps that need hand input
|
||||||
|
- Debug activation in physics mode while near, holding, or hand-holding interactions
|
||||||
|
- Hand snapshot context for R3F and UI consumers
|
||||||
|
- Fist detection
|
||||||
|
- Two-fists hold gesture for repair fragmentation
|
||||||
|
- Hand grab support for `GrabbableObject`
|
||||||
|
- Hand-tracking debug panel with status, source/server state, hand count, fist state, and glove model status
|
||||||
|
- SVG hand visualizer fallback
|
||||||
|
- `gant_l` and `gant_r` R3F glove overlays when tracking is active
|
||||||
|
|
||||||
## Debug Tooling
|
## Debug Tooling
|
||||||
|
|
||||||
- `?debug` query param enables the debug panel
|
- `?debug` query param enables debug systems
|
||||||
- `lil-gui` controls for camera mode, scene mode, and interaction spheres
|
- `lil-gui` root debug folder
|
||||||
- Debug scene helpers
|
- Camera mode switch between player and debug camera
|
||||||
- Free debug camera
|
- Scene mode switch between production game and physics test scene
|
||||||
- `r3f-perf` overlay
|
- R3F perf toggle
|
||||||
|
- Debug overlay toggle
|
||||||
|
- Hand-tracking source switch
|
||||||
|
- Interaction sphere debug toggle
|
||||||
|
- Grabbable tuning controls for stiffness, throw boost, and hold distance
|
||||||
|
- Debug helpers: grid and axes
|
||||||
|
- Debug camera controls
|
||||||
|
- Debug game-state panel
|
||||||
|
- Debug hand-tracking panel
|
||||||
|
- Physics test scene with floor, grabbable object, trigger object, repair zones, and animated model preview
|
||||||
|
- Animated `electricienne_animated` model preview restored in the debug physics scene
|
||||||
|
|
||||||
## Not Implemented Yet
|
## Map And Content Editor
|
||||||
|
|
||||||
- mission system
|
- `/editor` route
|
||||||
- zone system
|
- Automatic loading of `public/map.json`
|
||||||
- cinematic system
|
- Folder upload fallback when map data is not available
|
||||||
- dialogue system
|
- Shared `MapNode` format with runtime map loading
|
||||||
- loading flow
|
- Render available map models
|
||||||
- minimap and mission HUD
|
- Fallback cubes for missing models
|
||||||
- full production separation between gameplay and debug scenes
|
- Object selection by click
|
||||||
|
- Transform modes: translate, rotate, scale
|
||||||
|
- Transform keyboard shortcuts: `T`, `R`, `S`
|
||||||
|
- Selection lock
|
||||||
|
- Explicit selection clear
|
||||||
|
- Undo and redo
|
||||||
|
- Player-style editor navigation with `WASD`, `ZQSD`, arrows, `Space`, and `Shift`
|
||||||
|
- JSON inspector
|
||||||
|
- JSON export
|
||||||
|
- Dev-server save endpoint for `public/map.json`
|
||||||
|
- Dialogue manifest editor
|
||||||
|
- SRT subtitle editor
|
||||||
|
- Audio preview and cue timing helpers
|
||||||
|
- French SRT cue creation helper
|
||||||
|
- Dialogue asset validation endpoint
|
||||||
|
- Cinematic manifest editor
|
||||||
|
- Cinematic camera keyframe editor
|
||||||
|
- Cinematic dialogue cue editor
|
||||||
|
- Cinematic preview in the editor canvas
|
||||||
|
- Dev-server endpoints for dialogue, SRT, and cinematic saves
|
||||||
|
|
||||||
|
## In-App Documentation
|
||||||
|
|
||||||
|
- `/docs` documentation layout
|
||||||
|
- Markdown rendered through `react-markdown` and `remark-gfm`
|
||||||
|
- Technical docs for architecture, scene runtime, repair game, interaction, editor, audio, hand tracking, Zustand, Three debugging, animation, and target architecture
|
||||||
|
- User docs for implemented features, main feature, editor usage, and code-review preparation
|
||||||
|
|
||||||
|
## Not Implemented Or Incomplete
|
||||||
|
|
||||||
|
- Complete production mission manager/orchestrator
|
||||||
|
- Full mission HUD or minimap
|
||||||
|
- Full zone system
|
||||||
|
- Dialogue branching
|
||||||
|
- Production persistence for editor saves
|
||||||
|
- Production backend for repair-game runtime selection
|
||||||
|
- Production save/load of player progression
|
||||||
|
- Full migration of player movement to Rapier
|
||||||
|
- Advanced hand smoothing and calibrated glove finger animation
|
||||||
|
- Snap-to-grid, object creation, object deletion, material editing, or model editing in the map editor
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Main Feature
|
||||||
|
|
||||||
|
This document explains the current repair-game flow in La-Fabrik.
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
|
||||||
|
The main feature is a reusable repair flow mounted in the production game scene. It lets the player approach the active mission object, inspect it, fragment it, scan the broken part, install the correct replacement, validate completion, and move to the next mission state.
|
||||||
|
|
||||||
|
The current user flow is:
|
||||||
|
|
||||||
|
1. Enter a mission state such as `bike`, `pylone`, or `ferme`.
|
||||||
|
2. Move close to the active repair object in the game scene.
|
||||||
|
3. Aim at the object and press the interaction key when prompted.
|
||||||
|
4. The mission step moves from `waiting` to `inspected`.
|
||||||
|
5. The repair case appears near the mission object and can float when the player approaches it. A repair movement-lock rule exists in code, but it is currently disabled by the hook on `develop`.
|
||||||
|
6. Aim at the repair case and press `E`, or hold both fists closed for one second, to move from `inspected` to `fragmented`.
|
||||||
|
7. The mission object uses an exploded-model transition, then moves to `scanning`.
|
||||||
|
8. The scan visual moves across the fragmented model one part at a time and keeps a red marker plus the `cassé.webm` prompt centered on any configured broken part once it has been found.
|
||||||
|
9. In `repairing`, the case opens in a larger focused view and several grabbable replacement parts appear on the case placeholders.
|
||||||
|
10. Move the correct replacement part close to a placeholder. When released near a placeholder, it snaps into place with a short animation.
|
||||||
|
11. Move each scanned broken part into a compatible placeholder so the damaged parts are stored in the case.
|
||||||
|
12. Press `E` on the green install target to move to `reassembling`. Wrong parts turn the target red and cannot finish the repair.
|
||||||
|
13. The exploded object animates back into its assembled form with completion particles, then moves to `done`.
|
||||||
|
14. Press `E` on the completion target. The repair case closes, returns to the ground, disappears, then `completeMission` moves to the next mission or to `outro` after `ferme`.
|
||||||
|
|
||||||
|
## Why It Matters
|
||||||
|
|
||||||
|
This feature validates the repair loop before a full mission system exists. It tests whether repair objects, physical proximity, model selection, audio feedback, and exploded model visualization can work together in the 3D scene.
|
||||||
|
|
||||||
|
For implementation details, see `docs/technical/repair-game.md`.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
In `waiting`, the active mission renders its repair object and the `interagir.webm` prompt in the game scene. The interaction uses the shared focus/raycast interaction system, so the player still gets the normal `E` prompt.
|
||||||
|
|
||||||
|
When the player inspects the object, `RepairGame` writes `inspected` through the generic mission store action. The repair case then appears from the mission config with a small pop animation. When the player is close enough, the case model floats upward and rotates gently to signal interactivity. The codebase also contains a shared repair movement-lock hook and HTML indicator, but `useRepairMovementLocked()` currently returns `false`, so movement remains available during the repair flow on the current branch.
|
||||||
|
|
||||||
|
In `inspected`, `RepairGame` can also move to `fragmented`. Keyboard input goes through the shared focus/raycast interaction system on the repair case, so the player must be close enough and aim at the case before pressing `E`. The hand-tracking path still uses a two-fists hold gesture and is state-based, so it does not depend on being inside a local object interaction radius.
|
||||||
|
|
||||||
|
In `fragmented`, the repair object is rendered with `ExplodableModel`, then automatically advances to `scanning`. In `scanning`, the exploded model remains visible, a blue scan visual moves from part to part, and a red halo/wire marker plus the configured broken UI video stay attached to configured broken parts after the scanner reaches them. The scan can match a specific `nodeName` when mission data provides one, otherwise it falls back to the first scanned parts as placeholder broken parts. In `repairing`, the case opens in a larger focused transform, `RepairCaseModel` traverses the case GLTF for empty nodes named `placeholder_*`, several grabbable replacement parts appear on those placeholder positions, and releasing a part near a placeholder snaps it into place with a short GSAP animation. Scanned broken parts are also rendered as grabbable objects and must be deposited into a compatible placeholder before the final install target validates. If `brokenParts[].placeholderName` is configured, that broken part snaps only to the matching placeholder; otherwise it can use any available placeholder. If the current case asset has no placeholder nodes, the flow keeps using fallback focus positions. Replacement parts show green or red placement feedback after snapping, broken parts show stored feedback after deposit, and the install target gives a short blocked feedback if the player tries to validate too early. The install target only validates when the configured correct replacement part is placed and all scanned broken parts have been deposited. In `reassembling`, the exploded model animates back into its assembled position with green completion particles before the flow moves to `done`. In `done`, the repaired object remains visible with a completion target; validating closes the repair case first, then plays the case exit animation before advancing the global mission progression.
|
||||||
|
|
||||||
|
The mission config now carries the mission-specific variations. `bike` repairs one cooling core, `pylone` scans and stores both the lamp relay and a damaged panel with slower scan/reassembly timing, and `ferme` scans and stores an irrigation pump plus humidity sensor with faster scan/reassembly timing.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- `src/world/GameStageContent.tsx` mounts production `RepairGame` instances for `bike`, `pylone`, and `ferme`.
|
||||||
|
- `src/components/three/gameplay/RepairCompletionStep.tsx` renders the final repaired object, completion target, case exit animation, and mission UI prompt.
|
||||||
|
- `src/components/three/gameplay/RepairGame.tsx` composes the reusable production repair flow.
|
||||||
|
- `src/components/three/gameplay/RepairBrokenPartHighlight.tsx` renders the red halo and wire marker around detected broken parts during scanning.
|
||||||
|
- `src/components/three/gameplay/RepairBrokenPartPrompt.tsx` centers the configured broken UI video on detected broken parts during scanning.
|
||||||
|
- `src/components/three/gameplay/RepairInspectionObject.tsx` handles the `waiting` inspection interaction.
|
||||||
|
- `src/components/three/gameplay/RepairMissionCase.tsx` renders the mission repair case after inspection.
|
||||||
|
- `src/components/three/gameplay/RepairRepairingStep.tsx` renders grabbable replacement choices, grabbable scanned broken parts, placeholder placement markers, snap placement behavior, correct-part and broken-part placement validation, and the install trigger in `repairing`.
|
||||||
|
- `src/components/three/gameplay/RepairReassemblyStep.tsx` renders the inverse fragmentation animation before the final completion step.
|
||||||
|
- `src/components/three/gameplay/RepairCompletionParticles.tsx` renders the green completion particles during reassembly.
|
||||||
|
- `src/components/three/gameplay/RepairPromptVideo.tsx` renders `.webm` prompts inside the 3D scene.
|
||||||
|
- `src/components/three/gameplay/RepairScanSequence.tsx` keeps the exploded model visible and advances the scan from part to part.
|
||||||
|
- `src/components/three/gameplay/RepairScanVisual.tsx` renders the scan halo and scan line around the active part.
|
||||||
|
- `src/components/ui/RepairMovementLockIndicator.tsx` renders the HTML indicator intended for repair movement lock.
|
||||||
|
- `src/hooks/gameplay/useRepairFragmentationInput.ts` handles the `inspected -> fragmented` two-fists input and can optionally bind keyboard input for non-trigger flows.
|
||||||
|
- `src/hooks/gameplay/useRepairMissionStep.ts` reads the active mission step from the game store.
|
||||||
|
- `src/hooks/gameplay/useRepairMovementLocked.ts` exposes the shared repair movement-lock rule used by the player controller and UI indicator, but currently returns `false`.
|
||||||
|
- `src/hooks/handTracking/useBothFistsHold.ts` detects the reusable two-fists hold gesture.
|
||||||
|
- `src/components/three/gameplay/RepairCaseModel.tsx` renders and animates the case model, and exposes `placeholder_*` transforms when the GLTF provides them.
|
||||||
|
- `src/components/three/models/ExplodableModel.tsx` renders selectable models with split/exploded visualization.
|
||||||
|
- `src/data/gameplay/repairCaseConfig.ts` stores repair case model, sound, and animation constants.
|
||||||
|
- `src/data/gameplay/repairGameConfig.ts` stores repair flow timing constants.
|
||||||
|
- `src/data/gameplay/repairMissions.ts` stores reusable repair mission config for `bike`, `pylone`, and `ferme`.
|
||||||
|
- `src/managers/stores/useGameStore.ts` stores mission progression state and generic mission step helpers.
|
||||||
|
- `src/types/gameplay/repairMission.ts` contains shared repair mission ids, mission steps, and guards used by the store, data config, debug UI, and gameplay components.
|
||||||
|
|
||||||
|
## Runtime Requirements
|
||||||
|
|
||||||
|
The production repair flow currently requires:
|
||||||
|
|
||||||
|
- the active `mainState` to be one of `bike`, `pylone`, or `ferme`
|
||||||
|
- `GameStageContent` mounted inside the game scene Rapier `Physics` boundary
|
||||||
|
- model assets available under `public/models/`
|
||||||
|
- sound assets available under `public/sounds/`
|
||||||
|
|
||||||
|
Frontend command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Debug URL for state switching and inspection:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
http://localhost:5173/?debug
|
||||||
|
```
|
||||||
|
|
||||||
|
The debug physics scene keeps the existing grab, trigger, and animated model tests, and also exposes separate `Bike`, `Pylone`, and `Farm` repair playground zones. Use the debug game-state panel to switch `mainState`; selecting a locked repair mission in that panel opens it at `waiting`, and the matching repair zone mounts the same reusable `RepairGame` flow with that mission's model, broken parts, replacement parts, prompts, and timings.
|
||||||
|
|
||||||
|
## Related Hand Tracking
|
||||||
|
|
||||||
|
Hand tracking can move grabbable physics objects with webcam input in debug scenes. In the production repair flow, it is also used for the `inspected -> fragmented` transition through the two-fists hold gesture.
|
||||||
|
|
||||||
|
For hand tracking, run the Python backend separately:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source backend/.venv/bin/activate
|
||||||
|
python -m backend.main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current Limitations
|
||||||
|
|
||||||
|
- The reusable production `RepairGame` currently covers `waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission`.
|
||||||
|
- Mission progression is wired through Zustand using `completeMission` at the end of each repair.
|
||||||
|
- There is no central `GameManager` in this branch.
|
||||||
|
- Repair movement lock is currently disabled by `useRepairMovementLocked()`.
|
||||||
|
- Hand tracking is available for the two-fists input and grabbable repair parts; case interaction and final installation still use the shared `E` trigger path.
|
||||||
|
- The repair-game content is configured statically in `src/data/gameplay/`.
|
||||||
@@ -2,6 +2,7 @@ import js from "@eslint/js";
|
|||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import reactHooks from "eslint-plugin-react-hooks";
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
import reactRefresh from "eslint-plugin-react-refresh";
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import prettierRecommended from "eslint-plugin-prettier/recommended";
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
import { defineConfig, globalIgnores } from "eslint/config";
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
|
||||||
@@ -20,4 +21,5 @@ export default defineConfig([
|
|||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
prettierRecommended,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[phases.setup]
|
||||||
|
nixPkgs = ["nodejs"]
|
||||||
Generated
+1811
-461
File diff suppressed because it is too large
Load Diff
+18
-4
@@ -3,27 +3,36 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"dev:three-debug": "vite --mode three-debug --host 0.0.0.0",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mediapipe/tasks-vision": "^0.10.35",
|
||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.6.0",
|
"@react-three/fiber": "^9.6.1",
|
||||||
"@react-three/postprocessing": "^3.0.4",
|
|
||||||
"@react-three/rapier": "^2.2.0",
|
"@react-three/rapier": "^2.2.0",
|
||||||
|
"@tanstack/react-router": "^1.169.2",
|
||||||
"gsap": "^3.15.0",
|
"gsap": "^3.15.0",
|
||||||
"lil-gui": "^0.21.0",
|
"lil-gui": "^0.21.0",
|
||||||
|
"lucide-react": "^1.11.0",
|
||||||
"r3f-perf": "^7.2.3",
|
"r3f-perf": "^7.2.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"three": "^0.183.2"
|
"react-markdown": "^10.1.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"three": "0.182.0",
|
||||||
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
@@ -41,5 +50,10 @@
|
|||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.58.0",
|
"typescript-eslint": "^8.58.0",
|
||||||
"vite": "^8.0.4"
|
"vite": "^8.0.4"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"r3f-perf": {
|
||||||
|
"@react-three/drei": "$@react-three/drei"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"cinematics": [
|
||||||
|
{
|
||||||
|
"id": "intro_sequence",
|
||||||
|
"trigger": "intro_sequence",
|
||||||
|
"cameraKeyframes": [
|
||||||
|
{ "time": 0, "position": [8, 5, 12], "target": [0, 2, 0] },
|
||||||
|
{ "time": 8, "position": [12, 4, -6], "target": [10, 1.4, -8] },
|
||||||
|
{ "time": 16, "position": [5, 6, -15], "target": [0, 3, -20] },
|
||||||
|
{ "time": 24, "position": [0, 8, -30], "target": [0, 0, -40] }
|
||||||
|
],
|
||||||
|
"dialogueCues": [
|
||||||
|
{ "time": 0, "dialogueId": "intro_welcome" },
|
||||||
|
{ "time": 8, "dialogueId": "intro_explanation" },
|
||||||
|
{ "time": 16, "dialogueId": "intro_mission" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "intro_overview",
|
||||||
|
"timecode": 0,
|
||||||
|
"dialogueCues": [
|
||||||
|
{
|
||||||
|
"time": 0,
|
||||||
|
"dialogueId": "narrateur_bienvenueaaltera"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cameraKeyframes": [
|
||||||
|
{
|
||||||
|
"time": 0,
|
||||||
|
"position": [8, 5, 12],
|
||||||
|
"target": [0, 2, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": 4,
|
||||||
|
"position": [12, 4, -6],
|
||||||
|
"target": [10, 1.4, -8]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+35030
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user