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 aa6d43a..a417dbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,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 e9c0ac5..00d7630 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,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/three/AnimatedModel.tsx b/src/components/three/AnimatedModel.tsx index f9c5991..099f14c 100644 --- a/src/components/three/AnimatedModel.tsx +++ b/src/components/three/AnimatedModel.tsx @@ -151,7 +151,7 @@ export function AnimatedModel({ defaultAction.play(); // eslint-disable-next-line react-hooks/set-state-in-effect setIsReady(true); - // eslint-disable-next-line react-hooks/set-state-in-effect + setCurrentAnim(defaultAction.getClip().name); onLoaded?.(); } else { diff --git a/src/components/three/SimpleModel.tsx b/src/components/three/SimpleModel.tsx index cfa6e83..6c7b9c2 100644 --- a/src/components/three/SimpleModel.tsx +++ b/src/components/three/SimpleModel.tsx @@ -1,5 +1,5 @@ import { useGLTF } from "@react-three/drei"; -import type { Vector3Tuple } from "@/types/3d"; +import type { Vector3Tuple } from "@/types/three"; export interface SimpleModelConfig { modelPath: string; diff --git a/src/components/three/index.ts b/src/components/three/index.ts index d8516b8..9ff35d8 100644 --- a/src/components/three/index.ts +++ b/src/components/three/index.ts @@ -1,4 +1,4 @@ -export { AnimatedModel, useAnimatedModel } from "./AnimatedModel"; +export { AnimatedModel } from "./AnimatedModel"; export type { AnimatedModelConfig } from "./AnimatedModel"; export { SimpleModel } from "./SimpleModel"; 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..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..d7a95b3 100644 --- a/src/data/docs/docsTranslations.ts +++ b/src/data/docs/docsTranslations.ts @@ -207,6 +207,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 a32c152..763ef06 100644 --- a/src/index.css +++ b/src/index.css @@ -391,6 +391,77 @@ canvas { letter-spacing: 0.03em; } +.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..db0a093 --- /dev/null +++ b/src/managers/stores/useGameStore.ts @@ -0,0 +1,233 @@ +import { create } from "zustand"; + +export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro"; +export type MissionStep = + | "locked" + | "waiting" + | "inspected" + | "fragmented" + | "scanning" + | "repairing" + | "done"; + +export interface IntroState { + dialogueAudio: string | null; + hasCompleted: boolean; + isBikeUnlocked: boolean; +} + +export interface MissionState { + currentStep: MissionStep; + dialogueAudio: string | null; +} + +export 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; +} + +export 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 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 2635e63..dad9a41 100644 --- a/src/pages/page.tsx +++ b/src/pages/page.tsx @@ -1,8 +1,7 @@ import { Suspense } from "react"; import { Canvas } from "@react-three/fiber"; -import { Crosshair } from "@/components/ui/Crosshair"; -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 { @@ -14,8 +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 ( diff --git a/src/world/GameStageContent.tsx b/src/world/GameStageContent.tsx new file mode 100644 index 0000000..ca16da8 --- /dev/null +++ b/src/world/GameStageContent.tsx @@ -0,0 +1,44 @@ +import { useGameStore } from "@/managers/stores/useGameStore"; +import type { Vector3Tuple } from "@/types/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 b945c38..fe0344b 100644 --- a/src/world/World.tsx +++ b/src/world/World.tsx @@ -11,6 +11,7 @@ import { DebugHelpers } from "@/components/debug/scene/DebugHelpers"; import { Environment } from "@/world/Environment"; import { Lighting } from "@/world/Lighting"; import { GameMap } from "@/world/GameMap"; +import { GameStageContent } from "@/world/GameStageContent"; import { Player } from "@/world/player/Player"; import { TestScene } from "@/world/debug/TestScene"; @@ -31,7 +32,10 @@ export function World(): React.JSX.Element { {cameraMode === "debug" ? : null} {sceneMode === "game" ? ( - + <> + + + ) : ( )}