diff --git a/docs/technical/zustand.md b/docs/technical/zustand.md
new file mode 100644
index 0000000..8e05717
--- /dev/null
+++ b/docs/technical/zustand.md
@@ -0,0 +1,163 @@
+# Zustand Game State
+
+This document explains how Zustand is used in the current project.
+
+## Why Zustand Exists Here
+
+The project needs one shared source of truth for the player's progression through the experience.
+
+The current progression is split into main states:
+
+| Main state | Role |
+| ---------- | ------------------------------- |
+| `intro` | Onboarding and opening sequence |
+| `bike` | E-bike repair sequence |
+| `pylone` | Power grid sequence |
+| `ferme` | Vertical farm sequence |
+| `outro` | Ending sequence |
+
+Each main state can also own smaller sub state, such as the current mission step, dialogue audio, or completion flags.
+
+Zustand is useful because React and React Three Fiber components can subscribe only to the state slice they need. When that slice changes, only the subscribed components re-render.
+
+## Store Location
+
+The game progression store lives here:
+
+```txt
+src/managers/stores/useGameStore.ts
+```
+
+The store is placed under `src/managers/stores/` because it belongs to the gameplay orchestration layer, not to a specific visual component.
+
+## Managers vs Store
+
+Managers are responsible for local runtime objects and imperative behavior.
+
+Examples:
+
+- `AudioManager` owns audio elements and sound pools.
+- `InteractionManager` owns transient interaction handles and input-oriented behavior.
+
+Managers can read from or write to the Zustand store when their local behavior needs to affect global gameplay progression.
+
+The Zustand store is responsible for durable global state:
+
+- current main state
+- mission sub state
+- progression flags
+- dialogue/audio references
+- state transitions
+
+Rule of thumb:
+
+- manager = runtime objects, side effects, and local imperative logic
+- store = global gameplay state that UI or world components can subscribe to
+
+## Current Shape
+
+The store exposes:
+
+- `mainState`: the active game phase
+- `intro`: intro-specific state
+- `bike`: e-bike mission state
+- `pylone`: power grid mission state
+- `ferme`: farm mission state
+- `outro`: ending state
+- actions for direct updates and progression updates
+
+The mission steps currently use this sequence:
+
+```ts
+"locked" |
+ "waiting" |
+ "inspected" |
+ "fragmented" |
+ "scanning" |
+ "repairing" |
+ "done";
+```
+
+## 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
Current state: {mainState}
;
+}
+```
+
+This is better than reading the whole store, because the component re-renders only when `mainState` changes.
+
+## Updating 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`, or `completePylone`.
+
+## World Integration
+
+`src/world/GameStageContent.tsx` subscribes to `mainState` and mounts stage-specific content.
+
+That means the scene can progressively move toward this pattern:
+
+```tsx
+switch (mainState) {
+ case "intro":
+ return ;
+ case "bike":
+ return ;
+ case "pylone":
+ return ;
+ case "ferme":
+ return ;
+ case "outro":
+ return ;
+}
+```
+
+In React Three Fiber, mounting and unmounting JSX controls what appears in the Three.js scene. When a state-specific component disappears from JSX, React removes it from the scene.
+
+## UI Integration
+
+`src/components/ui/GameUI.tsx` groups the HTML overlays used by the playable route.
+
+Current overlays:
+
+- `GameStateHUD`: debug-only progression panel shown with `?debug`
+- `Crosshair`: player aiming helper
+- `InteractPrompt`: interaction prompt
+
+`src/pages/page.tsx` should stay thin and mount only the canvas and `GameUI`.
+
+## 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.
+
+## Next Steps
+
+The next natural step is to replace the temporary stage anchors in `GameStageContent` with real stage components, for example `IntroContent`, `BikeContent`, `PyloneContent`, `FermeContent`, and `OutroContent`.
diff --git a/package-lock.json b/package-lock.json
index 7a90dba..43b946c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,8 @@
"react-dom": "^19.2.4",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
- "three": "^0.183.2"
+ "three": "^0.183.2",
+ "zustand": "^5.0.12"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
diff --git a/package.json b/package.json
index f2d152b..ecf2f01 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,8 @@
"react-dom": "^19.2.4",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
- "three": "^0.183.2"
+ "three": "^0.183.2",
+ "zustand": "^5.0.12"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
diff --git a/src/components/ui/GameStateHUD.tsx b/src/components/ui/GameStateHUD.tsx
new file mode 100644
index 0000000..f38bafd
--- /dev/null
+++ b/src/components/ui/GameStateHUD.tsx
@@ -0,0 +1,75 @@
+import { Debug } from "@/utils/debug/Debug";
+import {
+ type MainGameState,
+ useGameStore,
+} from "@/managers/stores/useGameStore";
+
+const MAIN_STATES: MainGameState[] = [
+ "intro",
+ "bike",
+ "pylone",
+ "ferme",
+ "outro",
+];
+
+export function GameStateHUD(): React.JSX.Element | null {
+ const debug = Debug.getInstance();
+ const mainState = useGameStore((state) => state.mainState);
+ const detail = useGameStore((state) => {
+ switch (state.mainState) {
+ case "intro":
+ return state.intro.hasCompleted ? "completed" : "waiting";
+ case "bike":
+ return state.bike.currentStep;
+ case "pylone":
+ return state.pylone.currentStep;
+ case "ferme":
+ return state.ferme.currentStep;
+ case "outro":
+ return state.outro.hasStarted ? "started" : "waiting";
+ }
+ });
+ const setMainState = useGameStore((state) => state.setMainState);
+ const advanceGameState = useGameStore((state) => state.advanceGameState);
+ const resetGame = useGameStore((state) => state.resetGame);
+
+ if (!debug.active) return null;
+
+ return (
+
+ );
+}
diff --git a/src/components/ui/GameUI.tsx b/src/components/ui/GameUI.tsx
new file mode 100644
index 0000000..5a333e8
--- /dev/null
+++ b/src/components/ui/GameUI.tsx
@@ -0,0 +1,17 @@
+import { Crosshair } from "@/components/ui/Crosshair";
+import { GameStateHUD } from "@/components/ui/GameStateHUD";
+import { HandTrackingOverlay } from "@/components/ui/HandTrackingOverlay";
+import { HandTrackingVisualizer } from "@/components/ui/HandTrackingVisualizer";
+import { InteractPrompt } from "@/components/ui/InteractPrompt";
+
+export function GameUI(): React.JSX.Element {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/data/docs/docsSections.ts b/src/data/docs/docsSections.ts
index a1358f8..86bc869 100644
--- a/src/data/docs/docsSections.ts
+++ b/src/data/docs/docsSections.ts
@@ -44,6 +44,12 @@ export const docGroups: DocGroup[] = [
subtitle: "Webcam interaction pipeline",
meta: "05",
},
+ {
+ path: "/docs/zustand",
+ title: "Zustand Game State",
+ subtitle: "Progression store",
+ meta: "06",
+ },
],
},
{
@@ -53,25 +59,25 @@ export const docGroups: DocGroup[] = [
path: "/docs/features",
title: "Features",
subtitle: "Implemented scope",
- meta: "06",
+ meta: "07",
},
{
path: "/docs/main-feature",
title: "Main Feature",
- subtitle: "Hand grab prototype",
- meta: "07",
+ subtitle: "Repair-game prototype",
+ meta: "08",
},
{
path: "/docs/editor",
title: "Editor User Guide",
subtitle: "Editing workflow",
- meta: "08",
+ meta: "09",
},
{
path: "/docs/animation",
title: "Animation & 3D Model System",
subtitle: "Components and usage",
- meta: "07",
+ meta: "010",
},
],
},
diff --git a/src/data/docs/docsTranslations.ts b/src/data/docs/docsTranslations.ts
index b93d29c..48699fa 100644
--- a/src/data/docs/docsTranslations.ts
+++ b/src/data/docs/docsTranslations.ts
@@ -206,6 +206,165 @@ Ce document décrit l'architecture visée à moyen terme pour le projet.
- Les chemins runtime uniquement debug doivent être clairement marqués et faciles à retirer plus tard.
`;
+export const zustandFr = `# État de jeu Zustand
+
+Ce document explique comment Zustand est utilisé dans le projet actuel.
+
+## Pourquoi Zustand existe ici
+
+Le projet a besoin d'une source de vérité partagée pour suivre la progression du joueur dans l'expérience.
+
+La progression actuelle est découpée en main states :
+
+| Main state | Rôle |
+| --- | --- |
+| \`intro\` | Onboarding et séquence d'ouverture |
+| \`bike\` | Séquence de réparation du vélo électrique |
+| \`pylone\` | Séquence du réseau électrique |
+| \`ferme\` | Séquence de la ferme verticale |
+| \`outro\` | Séquence de fin |
+
+Chaque main state peut aussi posséder un sous-état plus fin, comme l'étape de mission courante, l'audio de dialogue ou des flags de complétion.
+
+Zustand est utile parce que les composants React et React Three Fiber peuvent s'abonner uniquement à la partie de state dont ils ont besoin. Quand cette partie change, seuls les composants abonnés se mettent à jour.
+
+## Emplacement du store
+
+Le store de progression du jeu vit ici :
+
+\`\`\`txt
+src/managers/stores/useGameStore.ts
+\`\`\`
+
+Le store est placé dans \`src/managers/stores/\` parce qu'il appartient à la couche d'orchestration gameplay, pas à un composant visuel précis.
+
+## Managers vs Store
+
+Les managers sont responsables des objets runtime locaux et des comportements impératifs.
+
+Exemples :
+
+- \`AudioManager\` possède les éléments audio et les pools de sons.
+- \`InteractionManager\` possède les handles d'interaction transitoires et la logique orientée input.
+
+Un manager peut lire ou mettre à jour le store Zustand quand son comportement local doit impacter la progression globale du jeu.
+
+Le store Zustand est responsable de l'état global durable :
+
+- main state courant
+- sous-état de mission
+- flags de progression
+- références de dialogue/audio
+- transitions de state
+
+Règle simple :
+
+- manager = objets runtime, effets de bord et logique impérative locale
+- store = état gameplay global auquel l'UI ou le world peuvent s'abonner
+
+## Forme actuelle
+
+Le store expose :
+
+- \`mainState\` : phase active du jeu
+- \`intro\` : état spécifique à l'intro
+- \`bike\` : état de la mission vélo
+- \`pylone\` : état de la mission réseau électrique
+- \`ferme\` : état de la mission ferme
+- \`outro\` : état de fin
+- des actions de mise à jour directe et des actions de progression
+
+Les étapes de mission utilisent actuellement cette séquence :
+
+\`\`\`ts
+"locked" | "waiting" | "inspected" | "fragmented" | "scanning" | "repairing" | "done"
+\`\`\`
+
+## Lire le state dans un composant
+
+Utilise des selectors pour lire uniquement ce dont le composant a besoin.
+
+\`\`\`tsx
+import { useGameStore } from "@/managers/stores/useGameStore";
+
+export function Example(): React.JSX.Element {
+ const mainState = useGameStore((state) => state.mainState);
+
+ return
State courant : {mainState}
;
+}
+\`\`\`
+
+C'est mieux que de lire tout le store, car le composant se re-render uniquement quand \`mainState\` change.
+
+## Mettre à jour le state
+
+Préfère les actions explicites du store.
+
+\`\`\`ts
+const advanceGameState = useGameStore((state) => state.advanceGameState);
+
+advanceGameState();
+\`\`\`
+
+Pour le développement et le debug, des setters directs existent aussi :
+
+\`\`\`ts
+const setMainState = useGameStore((state) => state.setMainState);
+
+setMainState("bike");
+\`\`\`
+
+Les setters directs sont pratiques pour les panneaux debug, mais le gameplay de production devrait préférer les actions métier comme \`advanceGameState\`, \`completeBike\` ou \`completePylone\`.
+
+## Intégration avec le World
+
+\`src/world/GameStageContent.tsx\` s'abonne à \`mainState\` et monte le contenu spécifique au state courant.
+
+La scène peut donc évoluer progressivement vers ce pattern :
+
+\`\`\`tsx
+switch (mainState) {
+ case "intro":
+ return ;
+ case "bike":
+ return ;
+ case "pylone":
+ return ;
+ case "ferme":
+ return ;
+ case "outro":
+ return ;
+}
+\`\`\`
+
+Dans React Three Fiber, monter ou démonter du JSX contrôle ce qui apparaît dans la scène Three.js. Quand un composant lié à un state disparaît du JSX, React le retire de la scène.
+
+## Intégration UI
+
+\`src/components/ui/GameUI.tsx\` regroupe les overlays HTML utilisés par la route jouable.
+
+Overlays actuels :
+
+- \`GameStateHUD\` : panneau de progression debug visible avec \`?debug\`
+- \`Crosshair\` : aide de visée joueur
+- \`InteractPrompt\` : prompt d'interaction
+
+\`src/pages/page.tsx\` doit rester fin et monter seulement le canvas et \`GameUI\`.
+
+## Règles anti-régression
+
+- Ne pas stocker les valeurs mises à jour à chaque frame dans Zustand.
+- Utiliser \`useRef\` pour les valeurs mutables fréquentes comme la vélocité joueur, les vecteurs temporaires ou les données de boucle d'animation.
+- Utiliser des selectors au lieu de lire tout le store dans les composants.
+- Garder les transitions gameplay dans les actions du store quand possible.
+- Garder les contrôles debug derrière \`?debug\`.
+- Ajouter du state uniquement quand une vraie fonctionnalité runtime en a besoin.
+
+## Prochaines étapes
+
+La prochaine étape naturelle est de remplacer les ancres temporaires de \`GameStageContent\` par de vrais composants de phase, par exemple \`IntroContent\`, \`BikeContent\`, \`PyloneContent\`, \`FermeContent\` et \`OutroContent\`.
+`;
+
export const featuresFr = `# Fonctionnalités implémentées
Ce document liste les fonctionnalités présentes dans le code actuel.
diff --git a/src/index.css b/src/index.css
index 415b0d5..e0e1b13 100644
--- a/src/index.css
+++ b/src/index.css
@@ -467,6 +467,77 @@ canvas {
background: #38bdf8;
}
+.game-state-hud {
+ position: fixed;
+ top: 18px;
+ right: 18px;
+ z-index: 20;
+ display: grid;
+ gap: 12px;
+ width: min(320px, calc(100vw - 36px));
+ padding: 14px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ border-radius: 18px;
+ background: rgba(4, 7, 13, 0.78);
+ box-shadow: 0 18px 60px rgba(0, 0, 0, 0.35);
+ color: #f8fafc;
+ backdrop-filter: blur(16px);
+}
+
+.game-state-hud__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.game-state-hud__header span,
+.game-state-hud__detail {
+ color: rgba(248, 250, 252, 0.68);
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.game-state-hud__header strong {
+ font-size: 16px;
+ letter-spacing: -0.03em;
+ text-transform: uppercase;
+}
+
+.game-state-hud__detail {
+ margin: 0;
+ text-transform: none;
+}
+
+.game-state-hud__states,
+.game-state-hud__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.game-state-hud button {
+ min-height: 32px;
+ padding: 0 10px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.08);
+ color: #f8fafc;
+ font-size: 12px;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.game-state-hud button:hover,
+.game-state-hud button:focus-visible,
+.game-state-hud button.is-active {
+ border-color: rgba(125, 211, 252, 0.75);
+ background: rgba(125, 211, 252, 0.18);
+ outline: none;
+}
+
/* Editor page */
.editor-container {
position: fixed;
diff --git a/src/managers/stores/useGameStore.ts b/src/managers/stores/useGameStore.ts
new file mode 100644
index 0000000..a47d505
--- /dev/null
+++ b/src/managers/stores/useGameStore.ts
@@ -0,0 +1,233 @@
+import { create } from "zustand";
+
+export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro";
+type MissionStep =
+ | "locked"
+ | "waiting"
+ | "inspected"
+ | "fragmented"
+ | "scanning"
+ | "repairing"
+ | "done";
+
+interface IntroState {
+ dialogueAudio: string | null;
+ hasCompleted: boolean;
+ isBikeUnlocked: boolean;
+}
+
+interface MissionState {
+ currentStep: MissionStep;
+ dialogueAudio: string | null;
+}
+
+interface GameState {
+ mainState: MainGameState;
+ intro: IntroState;
+ bike: MissionState & {
+ isRepaired: boolean;
+ };
+ pylone: MissionState & {
+ isPowered: boolean;
+ };
+ ferme: MissionState & {
+ irrigationFixed: boolean;
+ };
+ outro: {
+ dialogueAudio: string | null;
+ hasStarted: boolean;
+ };
+}
+
+interface GameActions {
+ setMainState: (mainState: MainGameState) => void;
+ setIntroState: (intro: Partial) => void;
+ setBikeState: (bike: Partial) => void;
+ setPyloneState: (pylone: Partial) => void;
+ setFermeState: (ferme: Partial) => void;
+ setOutroState: (outro: Partial) => void;
+ completeIntro: () => void;
+ completeBike: () => void;
+ completePylone: () => void;
+ completeFerme: () => void;
+ startOutro: () => void;
+ advanceGameState: () => void;
+ resetGame: () => void;
+}
+
+type GameStore = GameState & GameActions;
+type GameStateUpdate = Partial;
+
+function getNextMissionStep(step: MissionStep): MissionStep {
+ switch (step) {
+ case "locked":
+ return "waiting";
+ case "waiting":
+ return "inspected";
+ case "inspected":
+ return "fragmented";
+ case "fragmented":
+ return "scanning";
+ case "scanning":
+ return "repairing";
+ case "repairing":
+ case "done":
+ return "done";
+ }
+}
+
+function completeIntroState(state: GameState): GameStateUpdate {
+ return {
+ mainState: "bike",
+ intro: {
+ ...state.intro,
+ hasCompleted: true,
+ isBikeUnlocked: true,
+ },
+ bike: {
+ ...state.bike,
+ currentStep: "waiting",
+ },
+ };
+}
+
+function completeBikeState(state: GameState): GameStateUpdate {
+ return {
+ mainState: "pylone",
+ bike: {
+ ...state.bike,
+ currentStep: "done",
+ isRepaired: true,
+ },
+ pylone: {
+ ...state.pylone,
+ currentStep: "waiting",
+ },
+ };
+}
+
+function completePyloneState(state: GameState): GameStateUpdate {
+ return {
+ mainState: "ferme",
+ pylone: {
+ ...state.pylone,
+ currentStep: "done",
+ isPowered: true,
+ },
+ ferme: {
+ ...state.ferme,
+ currentStep: "waiting",
+ },
+ };
+}
+
+function completeFermeState(state: GameState): GameStateUpdate {
+ return {
+ mainState: "outro",
+ ferme: {
+ ...state.ferme,
+ currentStep: "done",
+ irrigationFixed: true,
+ },
+ outro: {
+ ...state.outro,
+ hasStarted: true,
+ },
+ };
+}
+
+function startOutroState(state: GameState): GameStateUpdate {
+ return {
+ mainState: "outro",
+ outro: {
+ ...state.outro,
+ hasStarted: true,
+ },
+ };
+}
+
+function createInitialGameState(): GameState {
+ return {
+ mainState: "intro",
+ intro: {
+ dialogueAudio: null,
+ hasCompleted: false,
+ isBikeUnlocked: false,
+ },
+ bike: {
+ currentStep: "waiting",
+ dialogueAudio: null,
+ isRepaired: false,
+ },
+ pylone: {
+ currentStep: "waiting",
+ dialogueAudio: null,
+ isPowered: false,
+ },
+ ferme: {
+ currentStep: "waiting",
+ dialogueAudio: null,
+ irrigationFixed: false,
+ },
+ outro: {
+ dialogueAudio: null,
+ hasStarted: false,
+ },
+ };
+}
+
+export const useGameStore = create()((set) => ({
+ ...createInitialGameState(),
+ setMainState: (mainState) => set({ mainState }),
+ setIntroState: (intro) =>
+ set((state) => ({ intro: { ...state.intro, ...intro } })),
+ setBikeState: (bike) =>
+ set((state) => ({ bike: { ...state.bike, ...bike } })),
+ setPyloneState: (pylone) =>
+ set((state) => ({ pylone: { ...state.pylone, ...pylone } })),
+ setFermeState: (ferme) =>
+ set((state) => ({ ferme: { ...state.ferme, ...ferme } })),
+ setOutroState: (outro) =>
+ set((state) => ({ outro: { ...state.outro, ...outro } })),
+ completeIntro: () => set(completeIntroState),
+ completeBike: () => set(completeBikeState),
+ completePylone: () => set(completePyloneState),
+ completeFerme: () => set(completeFermeState),
+ startOutro: () => set(startOutroState),
+ advanceGameState: () =>
+ set((state) => {
+ if (state.mainState === "intro") {
+ return completeIntroState(state);
+ }
+
+ if (state.mainState === "bike") {
+ const nextStep = getNextMissionStep(state.bike.currentStep);
+ if (nextStep === "done") {
+ return completeBikeState(state);
+ }
+
+ return { bike: { ...state.bike, currentStep: nextStep } };
+ }
+
+ if (state.mainState === "pylone") {
+ const nextStep = getNextMissionStep(state.pylone.currentStep);
+ if (nextStep === "done") {
+ return completePyloneState(state);
+ }
+
+ return { pylone: { ...state.pylone, currentStep: nextStep } };
+ }
+
+ if (state.mainState === "ferme") {
+ const nextStep = getNextMissionStep(state.ferme.currentStep);
+ if (nextStep === "done") {
+ return completeFermeState(state);
+ }
+
+ return { ferme: { ...state.ferme, currentStep: nextStep } };
+ }
+
+ return startOutroState(state);
+ }),
+ resetGame: () => set(createInitialGameState()),
+}));
diff --git a/src/pages/docs/animation/page.tsx b/src/pages/docs/animation/page.tsx
index 93975ce..fb5404b 100644
--- a/src/pages/docs/animation/page.tsx
+++ b/src/pages/docs/animation/page.tsx
@@ -6,7 +6,7 @@ export function DocsAnimationPage(): React.JSX.Element {
);
diff --git a/src/pages/docs/editor/page.tsx b/src/pages/docs/editor/page.tsx
index c479c89..d44f7cd 100644
--- a/src/pages/docs/editor/page.tsx
+++ b/src/pages/docs/editor/page.tsx
@@ -7,7 +7,7 @@ export function DocsEditorPage(): React.JSX.Element {
);
diff --git a/src/pages/docs/zustand/page.tsx b/src/pages/docs/zustand/page.tsx
new file mode 100644
index 0000000..529be3a
--- /dev/null
+++ b/src/pages/docs/zustand/page.tsx
@@ -0,0 +1,14 @@
+import zustand from "../../../../docs/technical/zustand.md?raw";
+import { DocsDocument } from "@/components/docs/DocsDocument";
+import { zustandFr } from "@/data/docs/docsTranslations";
+
+export function DocsZustandPage(): React.JSX.Element {
+ return (
+
+ );
+}
diff --git a/src/pages/page.tsx b/src/pages/page.tsx
index 21b2f5c..0c542ae 100644
--- a/src/pages/page.tsx
+++ b/src/pages/page.tsx
@@ -1,11 +1,8 @@
import { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
-import { Crosshair } from "@/components/ui/Crosshair";
-import { HandTrackingOverlay } from "@/components/ui/HandTrackingOverlay";
-import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
-import { HandTrackingVisualizer } from "@/components/ui/HandTrackingVisualizer";
-import { InteractPrompt } from "@/components/ui/InteractPrompt";
import { DebugPerf } from "@/components/debug/DebugPerf";
+import { GameUI } from "@/components/ui/GameUI";
+import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
import { World } from "@/world/World";
export function HomePage(): React.JSX.Element {
@@ -17,10 +14,7 @@ export function HomePage(): React.JSX.Element {
-
-
-
-
+
);
}
diff --git a/src/router.tsx b/src/router.tsx
index 1c3c09b..7be6634 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -17,6 +17,7 @@ import {
DocsReadmeRoute,
DocsTargetArchitectureRoute,
DocsTechnicalEditorRoute,
+ DocsZustandRoute,
} from "@/routes/DocsRoute";
const rootRoute = createRootRoute({
@@ -47,6 +48,7 @@ const docsChildRoutes = [
{ path: "target-architecture", component: DocsTargetArchitectureRoute },
{ path: "technical-editor", component: DocsTechnicalEditorRoute },
{ path: "hand-tracking", component: DocsHandTrackingRoute },
+ { path: "zustand", component: DocsZustandRoute },
{ path: "features", component: DocsFeaturesRoute },
{ path: "main-feature", component: DocsMainFeatureRoute },
{ path: "editor", component: DocsEditorRoute },
diff --git a/src/routes/DocsRoute.tsx b/src/routes/DocsRoute.tsx
index b8648da..b33f578 100644
--- a/src/routes/DocsRoute.tsx
+++ b/src/routes/DocsRoute.tsx
@@ -43,6 +43,10 @@ const LazyDocsHandTrackingPage = lazyNamed(
() => import("@/pages/docs/hand-tracking/page"),
"DocsHandTrackingPage",
);
+const LazyDocsZustandPage = lazyNamed(
+ () => import("@/pages/docs/zustand/page"),
+ "DocsZustandPage",
+);
const LazyDocsFeaturesPage = lazyNamed(
() => import("@/pages/docs/features/page"),
"DocsFeaturesPage",
@@ -84,6 +88,10 @@ export function DocsHandTrackingRoute(): React.JSX.Element {
return withDocsSuspense(LazyDocsHandTrackingPage);
}
+export function DocsZustandRoute(): React.JSX.Element {
+ return withDocsSuspense(LazyDocsZustandPage);
+}
+
export function DocsFeaturesRoute(): React.JSX.Element {
return withDocsSuspense(LazyDocsFeaturesPage);
}
diff --git a/src/world/GameStageContent.tsx b/src/world/GameStageContent.tsx
new file mode 100644
index 0000000..4d8c936
--- /dev/null
+++ b/src/world/GameStageContent.tsx
@@ -0,0 +1,44 @@
+import { useGameStore } from "@/managers/stores/useGameStore";
+import type { Vector3Tuple } from "@/types/three/three";
+
+interface StageAnchorProps {
+ color: string;
+ position: Vector3Tuple;
+ scale?: number;
+}
+
+function StageAnchor({
+ color,
+ position,
+ scale = 1,
+}: StageAnchorProps): React.JSX.Element {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export function GameStageContent(): React.JSX.Element {
+ const mainState = useGameStore((state) => state.mainState);
+
+ switch (mainState) {
+ case "intro":
+ return ;
+ case "bike":
+ return ;
+ case "pylone":
+ return ;
+ case "ferme":
+ return ;
+ case "outro":
+ return ;
+ }
+}
diff --git a/src/world/World.tsx b/src/world/World.tsx
index abed386..5a772e2 100644
--- a/src/world/World.tsx
+++ b/src/world/World.tsx
@@ -12,6 +12,7 @@ import { Environment } from "@/world/Environment";
import { GameMusic } from "@/world/GameMusic";
import { Lighting } from "@/world/Lighting";
import { GameMap } from "@/world/GameMap";
+import { GameStageContent } from "@/world/GameStageContent";
import { Player } from "@/world/player/Player";
import { TestMap } from "@/world/debug/TestMap";
@@ -35,6 +36,7 @@ export function World(): React.JSX.Element {
<>
+
>
) : (