diff --git a/docs/technical/zustand.md b/docs/technical/zustand.md
new file mode 100644
index 0000000..0913a70
--- /dev/null
+++ b/docs/technical/zustand.md
@@ -0,0 +1,133 @@
+# 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.
+
+## 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" | "inspect" | "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/src/components/ui/GameUI.tsx b/src/components/ui/GameUI.tsx
new file mode 100644
index 0000000..2581d02
--- /dev/null
+++ b/src/components/ui/GameUI.tsx
@@ -0,0 +1,13 @@
+import { Crosshair } from "@/components/ui/Crosshair";
+import { GameStateHUD } from "@/components/ui/GameStateHUD";
+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 8c4aad6..07343c6 100644
--- a/src/data/docs/docsSections.ts
+++ b/src/data/docs/docsSections.ts
@@ -38,6 +38,12 @@ export const docGroups: DocGroup[] = [
subtitle: "Implementation details",
meta: "04",
},
+ {
+ path: "/docs/zustand",
+ title: "Zustand Game State",
+ subtitle: "Progression store",
+ meta: "05",
+ },
],
},
{
@@ -47,19 +53,19 @@ export const docGroups: DocGroup[] = [
path: "/docs/features",
title: "Features",
subtitle: "Implemented scope",
- meta: "05",
+ meta: "06",
},
{
path: "/docs/editor",
title: "Editor User Guide",
subtitle: "Editing workflow",
- meta: "06",
+ meta: "07",
},
{
path: "/docs/animation",
title: "Animation & 3D Model System",
subtitle: "Components and usage",
- meta: "07",
+ meta: "08",
},
],
},
diff --git a/src/data/docs/docsTranslations.ts b/src/data/docs/docsTranslations.ts
index 82d45ec..6c1ad7d 100644
--- a/src/data/docs/docsTranslations.ts
+++ b/src/data/docs/docsTranslations.ts
@@ -207,6 +207,141 @@ 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.
+
+## 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" | "inspect" | "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/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 53c7236..deb3960 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/features/page.tsx b/src/pages/docs/features/page.tsx
index 4b41580..8010b5f 100644
--- a/src/pages/docs/features/page.tsx
+++ b/src/pages/docs/features/page.tsx
@@ -7,7 +7,7 @@ export function DocsFeaturesPage(): 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 a601019..dad9a41 100644
--- a/src/pages/page.tsx
+++ b/src/pages/page.tsx
@@ -1,9 +1,7 @@
import { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
-import { Crosshair } from "@/components/ui/Crosshair";
-import { GameStateHUD } from "@/components/ui/GameStateHUD";
-import { InteractPrompt } from "@/components/ui/InteractPrompt";
import { DebugPerf } from "@/components/debug/DebugPerf";
+import { GameUI } from "@/components/ui/GameUI";
import { World } from "@/world/World";
export function HomePage(): React.JSX.Element {
@@ -15,9 +13,7 @@ export function HomePage(): React.JSX.Element {
-
-
-
+
>
);
}
diff --git a/src/router.tsx b/src/router.tsx
index d17b5c1..6e53dcd 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -15,6 +15,7 @@ import {
DocsReadmeRoute,
DocsTargetArchitectureRoute,
DocsTechnicalEditorRoute,
+ DocsZustandRoute,
} from "@/routes/docs/DocsRouteComponents";
const rootRoute = createRootRoute({
@@ -44,6 +45,7 @@ const docsChildRoutes = [
{ path: "architecture", component: DocsArchitectureRoute },
{ path: "target-architecture", component: DocsTargetArchitectureRoute },
{ path: "technical-editor", component: DocsTechnicalEditorRoute },
+ { path: "zustand", component: DocsZustandRoute },
{ path: "features", component: DocsFeaturesRoute },
{ path: "editor", component: DocsEditorRoute },
{ path: "animation", component: DocsAnimationRoute },
diff --git a/src/routes/docs/DocsRouteComponents.tsx b/src/routes/docs/DocsRouteComponents.tsx
index 51f96bd..3e591ea 100644
--- a/src/routes/docs/DocsRouteComponents.tsx
+++ b/src/routes/docs/DocsRouteComponents.tsx
@@ -30,6 +30,12 @@ const LazyDocsTechnicalEditorPage = lazy(() =>
})),
);
+const LazyDocsZustandPage = lazy(() =>
+ import("@/pages/docs/zustand/page").then((module) => ({
+ default: module.DocsZustandPage,
+ })),
+);
+
const LazyDocsFeaturesPage = lazy(() =>
import("@/pages/docs/features/page").then((module) => ({
default: module.DocsFeaturesPage,
@@ -88,6 +94,14 @@ export function DocsTechnicalEditorRoute(): React.JSX.Element {
);
}
+export function DocsZustandRoute(): React.JSX.Element {
+ return (
+
+
+
+ );
+}
+
export function DocsFeaturesRoute(): React.JSX.Element {
return (