From f7b968abe7efa1d2b628a577d9e6530ad88f096d Mon Sep 17 00:00:00 2001 From: math-pixel <59537610+math-pixel@users.noreply.github.com> Date: Mon, 11 May 2026 16:46:22 +0200 Subject: [PATCH] wip mission 2 --- GAME_FLOW.md | 119 +++++++++++++++---- package-lock.json | 39 +----- package.json | 3 +- src/App.tsx | 23 +++- src/components/3d/CentralObject.tsx | 60 ++++++++++ src/components/3d/VillageoisHelperObject.tsx | 53 +++++++++ src/components/game/GameFlow.tsx | 57 ++++++--- src/components/ui/DialogMessage.tsx | 54 +++++++++ src/components/ui/IntroUI.tsx | 11 +- src/components/zone/ZoneDetection.tsx | 30 +++-- src/data/audioConfig.ts | 3 + src/data/zones.ts | 9 +- src/hooks/useActivityCity.ts | 5 + src/hooks/useDialog.ts | 27 +++++ src/stateManager/GameStepManager.ts | 4 + src/stores/gameStore.ts | 30 +++++ src/types/game.ts | 6 +- src/world/World.tsx | 4 + src/world/player/PlayerController.tsx | 6 +- 19 files changed, 449 insertions(+), 94 deletions(-) create mode 100644 src/components/3d/CentralObject.tsx create mode 100644 src/components/3d/VillageoisHelperObject.tsx create mode 100644 src/components/ui/DialogMessage.tsx create mode 100644 src/hooks/useActivityCity.ts create mode 100644 src/hooks/useDialog.ts create mode 100644 src/stores/gameStore.ts diff --git a/GAME_FLOW.md b/GAME_FLOW.md index c3ed79c..ed68f79 100644 --- a/GAME_FLOW.md +++ b/GAME_FLOW.md @@ -3,7 +3,7 @@ ## Étapes du jeu ``` -intro → start-intro → naming → bienvenue → star-move → bike +intro → start-intro → naming → bienvenue → star-move → mission2 → searching_problem → preparation → outOfFabrik ``` --- @@ -14,11 +14,12 @@ intro → start-intro → naming → bienvenue → star-move → bike - É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 via `AudioManager.playSoundWithCallback()` +- **Action** : Joue l'audio d'intro (`intro`) - **Attente** : Attend que l'audio se termine - **Transition** : Vers `naming` quand l'audio se termine @@ -45,25 +46,50 @@ intro → start-intro → naming → bienvenue → star-move → bike - **État** : Le joueur peut maintenant se déplacer librement - **Zone** : La détection de zone devient active (ZoneDetection) -### 6. `bike` +### 6. `mission2` -- **Déclenchement** : Quand le joueur entre dans la zone de sortie +- **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 -- **Zone** : Détectée par `ZoneDetection` quand le joueur approche de la position configurée --- ## Fichiers clés -| Fichier | Rôle | -| ---------------------------------------- | ------------------------------------------------------------- | -| `src/stateManager/GameStepManager.ts` | Gère l'état global du jeu (étape actuelle, prénom, mouvement) | -| `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 | -| `src/components/ui/BienvenueDisplay.tsx` | Affiche le message de bienvenue | -| `src/components/zone/ZoneDetection.tsx` | Détecte quand le joueur entre dans une zone | -| `src/data/audioConfig.ts` | Chemins des fichiers audio | -| `src/data/zones.ts` | Configuration des zones de transition | +| 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 | --- @@ -72,8 +98,10 @@ intro → start-intro → naming → bienvenue → star-move → bike ```typescript // src/data/audioConfig.ts export const AUDIO_PATHS = { - intro: "/sounds/fa.mp3", // Audio joué pendant start-intro - bienvenue: "/sounds/fa.mp3", // Audio joué pendant bienvenue + intro: "/sounds/fa.mp3", + bienvenue: "/sounds/fa.mp3", + alertCentral: "/sounds/fa.mp3", + searchingProblem: "/sounds/fa.mp3", }; ``` @@ -86,29 +114,74 @@ export const AUDIO_PATHS = { export const ZONES: Zone[] = [ { id: "fabrikExit", - position: [50, 0, 50], // Position de la zone de sortie - radius: 10, // Rayon de détection - height: 20, // Hauteur de la zone (pour la visualisation) - targetStep: "bike", // Étape cible quand on entre dans la zone + 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 +- **Zone Visualization** : Anneaux visuels au sol pour les zones + cylindres transparents --- ## Notes techniques - Le mouvement du joueur est bloqué tant que `canMove` est `false` -- `useSyncExternalStore` est utilisé pour synchroniser l'état du jeu avec React -- Les transitions sont gérées par le `GameStepManager` via le pattern singleton +- 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 diff --git a/package-lock.json b/package-lock.json index 4fbc348..fa74fed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "r3f-perf": "^7.2.3", "react": "^19.2.4", "react-dom": "^19.2.4", - "three": "^0.183.2" + "three": "^0.183.2", + "zustand": "^5.0.13" }, "devDependencies": { "@eslint/js": "^9.39.4", @@ -902,9 +903,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -922,9 +920,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -942,9 +937,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -962,9 +954,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -982,9 +971,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1002,9 +988,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2751,9 +2734,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2775,9 +2755,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2799,9 +2776,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2823,9 +2797,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4109,9 +4080,9 @@ } }, "node_modules/zustand": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", - "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", + "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", "license": "MIT", "engines": { "node": ">=12.20.0" diff --git a/package.json b/package.json index 3a737f0..5ae9638 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "r3f-perf": "^7.2.3", "react": "^19.2.4", "react-dom": "^19.2.4", - "three": "^0.183.2" + "three": "^0.183.2", + "zustand": "^5.0.13" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/src/App.tsx b/src/App.tsx index 0066013..56524af 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,26 @@ -import { Suspense } from "react"; +import { Suspense, useEffect } from "react"; import { Canvas } from "@react-three/fiber"; import { Crosshair } from "@/components/ui/Crosshair"; import { InteractPrompt } from "@/components/ui/InteractPrompt"; import { IntroUI, BienvenueDisplay } from "@/components/ui/IntroUI"; +import { DialogMessage } from "@/components/ui/DialogMessage"; +import { useGameStore } from "@/stores/gameStore"; import { DebugPerf } from "@/utils/debug/DebugPerf"; import { World } from "@/world/World"; function App(): React.JSX.Element { + const dialogMessage = useGameStore((state) => state.dialogMessage); + const hideDialog = useGameStore((state) => state.hideDialog); + + useEffect(() => { + if (dialogMessage) { + const timer = setTimeout(() => { + hideDialog(); + }, 3000); + return () => clearTimeout(timer); + } + }, [dialogMessage, hideDialog]); + return ( <> @@ -19,6 +33,13 @@ function App(): React.JSX.Element { + {dialogMessage && ( + + )} ); } diff --git a/src/components/3d/CentralObject.tsx b/src/components/3d/CentralObject.tsx new file mode 100644 index 0000000..485784e --- /dev/null +++ b/src/components/3d/CentralObject.tsx @@ -0,0 +1,60 @@ +import { InteractableObject } from "@/components/3d/InteractableObject"; +import { useGameStore } from "@/stores/gameStore"; +import { Debug } from "@/utils/debug/Debug"; +import type { Vector3Tuple } from "@/types/3d"; + +interface CentralObjectProps { + position: Vector3Tuple; +} + +export function CentralObject({ + position, +}: CentralObjectProps): React.JSX.Element { + const step = useGameStore((state) => state.step); + const setStep = useGameStore((state) => state.setStep); + const setCanMove = useGameStore((state) => state.setCanMove); + const showDialog = useGameStore((state) => state.showDialog); + const debug = Debug.getInstance(); + + const handlePress = (): void => { + console.log("[CentralObject] handlePress called, current step:", step); + + if (step === "helped") { + console.log("[CentralObject] Transitioning to manipulation"); + setCanMove(false); + setStep("manipulation"); + } else if (step === "searching") { + console.log("[CentralObject] Showing help message"); + showDialog( + "Cet objet est trop lourd pour le porter tout seul, trouve de l'aide", + ); + } else { + console.log("[CentralObject] Step is not helped or searching, skipping"); + } + }; + + const shouldShow = + step === "helped" || step === "manipulation" || debug.active; + + if (!shouldShow) { + return <>; + } + + console.log("[CentralObject] Rendering, step:", step, "position:", position); + + return ( + + + + + + + + + ); +} diff --git a/src/components/3d/VillageoisHelperObject.tsx b/src/components/3d/VillageoisHelperObject.tsx new file mode 100644 index 0000000..72c27a0 --- /dev/null +++ b/src/components/3d/VillageoisHelperObject.tsx @@ -0,0 +1,53 @@ +import { InteractableObject } from "@/components/3d/InteractableObject"; +import { useGameStore } from "@/stores/gameStore"; +import { Debug } from "@/utils/debug/Debug"; +import type { Vector3Tuple } from "@/types/3d"; + +interface VillageoisHelperObjectProps { + position: Vector3Tuple; +} + +export function VillageoisHelperObject({ + position, +}: VillageoisHelperObjectProps): React.JSX.Element { + const step = useGameStore((state) => state.step); + const setStep = useGameStore((state) => state.setStep); + const debug = Debug.getInstance(); + + const handlePress = (): void => { + console.log("[VillageoisHelper] handlePress called, current step:", step); + if (step === "searching") { + console.log("[VillageoisHelper] Transitioning to helped"); + setStep("helped"); + } + }; + + const shouldShow = step === "searching" || debug.active; + + if (!shouldShow) { + return <>; + } + + console.log( + "[VillageoisHelper] Rendering, step:", + step, + "position:", + position, + ); + + return ( + + + + + + + + + ); +} diff --git a/src/components/game/GameFlow.tsx b/src/components/game/GameFlow.tsx index 597382e..c1a03df 100644 --- a/src/components/game/GameFlow.tsx +++ b/src/components/game/GameFlow.tsx @@ -1,28 +1,23 @@ -import { useEffect, useRef, useState } from "react"; -import { GameStepManager } from "@/stateManager/GameStepManager"; +import { useEffect, useRef } from "react"; +import { useGameStore } from "@/stores/gameStore"; import { AudioManager } from "@/stateManager/AudioManager"; import { AUDIO_PATHS } from "@/data/audioConfig"; export function GameFlow(): null { - const manager = GameStepManager.getInstance(); + const step = useGameStore((state) => state.step); + const setStep = useGameStore((state) => state.setStep); + const setActivityCity = useGameStore((state) => state.setActivityCity); + const setCanMove = useGameStore((state) => state.setCanMove); const hasInitialized = useRef(false); - const [step, setStep] = useState(manager.getStep()); - - useEffect(() => { - const unsubscribe = manager.subscribe(() => { - setStep(manager.getStep()); - }); - return unsubscribe; - }, [manager]); useEffect(() => { console.log("[GameFlow] Current step:", step); if (!hasInitialized.current && step === "intro") { hasInitialized.current = true; console.log("[GameFlow] Transition to start-intro"); - manager.transitionTo("start-intro"); + setStep("start-intro"); } - }, [step, manager]); + }, [step, setStep]); useEffect(() => { console.log("[GameFlow] useEffect triggered, step:", step); @@ -32,7 +27,7 @@ export function GameFlow(): null { const audio = AudioManager.getInstance(); audio.playSoundWithCallback(AUDIO_PATHS.intro, 0.5, () => { console.log("[GameFlow] Intro audio ended, transition to naming"); - manager.transitionTo("naming"); + setStep("naming"); }); return () => {}; @@ -43,15 +38,43 @@ export function GameFlow(): null { const audio = AudioManager.getInstance(); audio.playSoundWithCallback(AUDIO_PATHS.bienvenue, 0.5, () => { console.log("[GameFlow] Bienvenue audio ended, enable movement"); - manager.setCanMove(true); - manager.transitionTo("star-move"); + setCanMove(true); + setStep("star-move"); }); return () => {}; } + if (step === "mission2") { + console.log("[GameFlow] mission2 - setting activityCity to false"); + setActivityCity(false); + const audio = AudioManager.getInstance(); + audio.playSound(AUDIO_PATHS.alertCentral, 0.5); + } + + if (step === "searching") { + console.log("[GameFlow] Playing searching audio"); + const audio = AudioManager.getInstance(); + audio.playSoundWithCallback(AUDIO_PATHS.searching, 0.5, () => { + console.log("[GameFlow] searching audio ended"); + }); + + return () => {}; + } + + if (step === "helped") { + console.log("[GameFlow] Playing helped audio"); + const audio = AudioManager.getInstance(); + audio.playSound(AUDIO_PATHS.helped, 0.5); + } + + if (step === "manipulation") { + console.log("[GameFlow] manipulation - blocking movement"); + setCanMove(false); + } + return undefined; - }, [step, manager]); + }, [step, setStep, setActivityCity, setCanMove]); return null; } diff --git a/src/components/ui/DialogMessage.tsx b/src/components/ui/DialogMessage.tsx new file mode 100644 index 0000000..f729c4d --- /dev/null +++ b/src/components/ui/DialogMessage.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; + +interface DialogMessageProps { + message: string; + duration?: number; + onClose?: () => void; +} + +export function DialogMessage({ + message, + duration = 3000, + onClose, +}: DialogMessageProps): React.JSX.Element | null { + const [visible, setVisible] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setVisible(false); + onClose?.(); + }, duration); + + return () => clearTimeout(timer); + }, [duration, onClose]); + + if (!visible) return null; + + return ( +
+

+ {message} +

+
+ ); +} diff --git a/src/components/ui/IntroUI.tsx b/src/components/ui/IntroUI.tsx index 10ffaad..cb20c48 100644 --- a/src/components/ui/IntroUI.tsx +++ b/src/components/ui/IntroUI.tsx @@ -1,8 +1,10 @@ import { useState } from "react"; -import { useGameStep } from "@/hooks/useGameStep"; +import { useGameStore } from "@/stores/gameStore"; export function IntroUI(): React.JSX.Element | null { - const { step, setPlayerName, transitionTo } = useGameStep(); + const step = useGameStore((state) => state.step); + const setPlayerName = useGameStore((state) => state.setPlayerName); + const setStep = useGameStore((state) => state.setStep); const [inputValue, setInputValue] = useState(""); if (step !== "naming") return null; @@ -13,7 +15,7 @@ export function IntroUI(): React.JSX.Element | null { console.log("[IntroUI] Submitting, name:", inputValue.trim()); setPlayerName(inputValue.trim()); console.log("[IntroUI] Calling transitionTo('bienvenue')"); - transitionTo("bienvenue"); + setStep("bienvenue"); console.log("[IntroUI] After transitionTo, step should be:", step); }; @@ -98,7 +100,8 @@ export function IntroUI(): React.JSX.Element | null { } export function BienvenueDisplay(): React.JSX.Element | null { - const { step, playerName } = useGameStep(); + const step = useGameStore((state) => state.step); + const playerName = useGameStore((state) => state.playerName); if (step !== "bienvenue") return null; diff --git a/src/components/zone/ZoneDetection.tsx b/src/components/zone/ZoneDetection.tsx index 843bbdb..4662482 100644 --- a/src/components/zone/ZoneDetection.tsx +++ b/src/components/zone/ZoneDetection.tsx @@ -3,16 +3,31 @@ import { useFrame, useThree } from "@react-three/fiber"; import * as THREE from "three"; import { ZONES } from "@/data/zones"; import { GameStepManager } from "@/stateManager/GameStepManager"; +import { useGameStore } from "@/stores/gameStore"; import { Debug } from "@/utils/debug/Debug"; +import type { GameStep } from "@/types/game"; const _playerPos = new THREE.Vector3(); const _zonePos = new THREE.Vector3(); +const GAME_STEPS: GameStep[] = [ + "intro", + "start-intro", + "naming", + "bienvenue", + "star-move", + "mission2", + "searching_problem", + "preparation", + "outOfFabrik", +]; + export function ZoneDetection(): null { const camera = useThree((state) => state.camera); const manager = GameStepManager.getInstance(); const triggeredZones = useRef>(new Set()); const debug = Debug.getInstance(); + const step = useGameStore((state) => state.step); useEffect(() => { if (!debug.active) return; @@ -20,20 +35,17 @@ export function ZoneDetection(): null { const folder = debug.createFolder("Game"); if (!folder) return; - const gameState = { step: manager.getStep() }; + const gameState = { step: step }; const playerPos = { x: 0, y: 0, z: 0 }; - folder - .add(gameState, "step", ["intro", "bike"]) - .name("Game Step") - .disable(); + folder.add(gameState, "step", GAME_STEPS).name("Game Step").disable(); folder.add(playerPos, "x").name("Player X").listen().disable(); folder.add(playerPos, "y").name("Player Y").listen().disable(); folder.add(playerPos, "z").name("Player Z").listen().disable(); - const unsubManager = manager.subscribe(() => { - gameState.step = manager.getStep(); + const unsubStore = useGameStore.subscribe((state) => { + gameState.step = state.step; folder.controllersRecursive().forEach((c) => c.updateDisplay()); }); @@ -51,9 +63,9 @@ export function ZoneDetection(): null { return () => { cancelAnimationFrame(frameId); debug.destroyFolder("Game"); - unsubManager(); + unsubStore(); }; - }, [debug, manager, camera]); + }, [debug, camera, step]); useFrame(() => { camera.getWorldPosition(_playerPos); diff --git a/src/data/audioConfig.ts b/src/data/audioConfig.ts index 6a6edd1..bc86196 100644 --- a/src/data/audioConfig.ts +++ b/src/data/audioConfig.ts @@ -1,4 +1,7 @@ export const AUDIO_PATHS = { intro: "/sounds/fa.mp3", bienvenue: "/sounds/fa.mp3", + alertCentral: "/sounds/fa.mp3", + searching: "/sounds/fa.mp3", + helped: "/sounds/fa.mp3", } as const; diff --git a/src/data/zones.ts b/src/data/zones.ts index 974b67f..5ed57e9 100644 --- a/src/data/zones.ts +++ b/src/data/zones.ts @@ -7,6 +7,13 @@ export const ZONES: Zone[] = [ position: [-5, 25, -15] as Vector3Tuple, radius: 10, height: 20, - targetStep: "bike", + targetStep: "mission2", + }, + { + id: "searchingZone", + position: [-5, 25, -30] as Vector3Tuple, + radius: 10, + height: 20, + targetStep: "searching", }, ]; diff --git a/src/hooks/useActivityCity.ts b/src/hooks/useActivityCity.ts new file mode 100644 index 0000000..8df853a --- /dev/null +++ b/src/hooks/useActivityCity.ts @@ -0,0 +1,5 @@ +import { useGameStore } from "@/stores/gameStore"; + +export function useActivityCity(): boolean { + return useGameStore((state) => state.activityCity); +} diff --git a/src/hooks/useDialog.ts b/src/hooks/useDialog.ts new file mode 100644 index 0000000..40cedbb --- /dev/null +++ b/src/hooks/useDialog.ts @@ -0,0 +1,27 @@ +import { useState } from "react"; + +interface DialogState { + message: string; + visible: boolean; +} + +export function useDialog(): { + dialog: DialogState; + showDialog: (message: string) => void; + hideDialog: () => void; +} { + const [dialog, setDialog] = useState({ + message: "", + visible: false, + }); + + const showDialog = (message: string): void => { + setDialog({ message, visible: true }); + }; + + const hideDialog = (): void => { + setDialog((prev) => ({ ...prev, visible: false })); + }; + + return { dialog, showDialog, hideDialog }; +} diff --git a/src/stateManager/GameStepManager.ts b/src/stateManager/GameStepManager.ts index 8d56988..1141dc2 100644 --- a/src/stateManager/GameStepManager.ts +++ b/src/stateManager/GameStepManager.ts @@ -1,4 +1,5 @@ import type { GameStep, GameStepSnapshot } from "@/types/game"; +import { useGameStore } from "@/stores/gameStore"; export class GameStepManager { private static _instance: GameStepManager | null = null; @@ -49,6 +50,7 @@ export class GameStepManager { this._currentStep = step; this._cachedSnapshot = null; + useGameStore.getState().setStep(step); this._emit(); } @@ -57,6 +59,7 @@ export class GameStepManager { this._playerName = name; this._cachedSnapshot = null; + useGameStore.getState().setPlayerName(name); this._emit(); } @@ -65,6 +68,7 @@ export class GameStepManager { this._canMove = canMove; this._cachedSnapshot = null; + useGameStore.getState().setCanMove(canMove); this._emit(); } diff --git a/src/stores/gameStore.ts b/src/stores/gameStore.ts new file mode 100644 index 0000000..4afc69e --- /dev/null +++ b/src/stores/gameStore.ts @@ -0,0 +1,30 @@ +import { create } from "zustand"; +import type { GameStep } from "@/types/game"; + +interface GameState { + step: GameStep; + activityCity: boolean; + playerName: string; + canMove: boolean; + dialogMessage: string | null; + setStep: (step: GameStep) => void; + setActivityCity: (value: boolean) => void; + setPlayerName: (name: string) => void; + setCanMove: (canMove: boolean) => void; + showDialog: (message: string) => void; + hideDialog: () => void; +} + +export const useGameStore = create((set) => ({ + step: "intro", + activityCity: true, + playerName: "", + canMove: false, + dialogMessage: null, + setStep: (step) => set({ step }), + setActivityCity: (value) => set({ activityCity: value }), + setPlayerName: (name) => set({ playerName: name }), + setCanMove: (canMove) => set({ canMove }), + showDialog: (message) => set({ dialogMessage: message }), + hideDialog: () => set({ dialogMessage: null }), +})); diff --git a/src/types/game.ts b/src/types/game.ts index 543e7df..f6b5ffe 100644 --- a/src/types/game.ts +++ b/src/types/game.ts @@ -6,7 +6,11 @@ export type GameStep = | "naming" | "bienvenue" | "star-move" - | "bike"; + | "mission2" + | "searching" + | "helped" + | "manipulation" + | "outOfFabrik"; export interface Zone { id: string; diff --git a/src/world/World.tsx b/src/world/World.tsx index 798af29..895abf7 100644 --- a/src/world/World.tsx +++ b/src/world/World.tsx @@ -11,6 +11,8 @@ import { ZoneDetection, } from "@/components/zone/ZoneDetection"; import { GameFlow } from "@/components/game/GameFlow"; +import { CentralObject } from "@/components/3d/CentralObject"; +import { VillageoisHelperObject } from "@/components/3d/VillageoisHelperObject"; import { DebugCameraControls } from "@/utils/debug/scene/DebugCameraControls"; import { DebugHelpers } from "@/utils/debug/scene/DebugHelpers"; import { Environment } from "@/world/Environment"; @@ -36,6 +38,8 @@ export function World(): React.JSX.Element { + + {cameraMode === "debug" ? : null} {sceneMode === "game" ? ( diff --git a/src/world/player/PlayerController.tsx b/src/world/player/PlayerController.tsx index 50ff41d..500a9e4 100644 --- a/src/world/player/PlayerController.tsx +++ b/src/world/player/PlayerController.tsx @@ -24,7 +24,7 @@ import { PLAYER_XZ_DAMPING_FACTOR, } from "@/data/playerConfig"; import { InteractionManager } from "@/stateManager/InteractionManager"; -import { GameStepManager } from "@/stateManager/GameStepManager"; +import { useGameStore } from "@/stores/gameStore"; import type { Vector3Tuple } from "@/types/3d"; type Keys = { @@ -64,7 +64,7 @@ export function PlayerController({ const velocity = useRef(new THREE.Vector3()); const onFloor = useRef(false); const wantsJump = useRef(false); - const gameStepManager = GameStepManager.getInstance(); + const canMove = useGameStore((state) => state.canMove); const capsule = useRef( new Capsule( @@ -167,7 +167,7 @@ export function PlayerController({ }, []); useFrame((_, delta) => { - if (!gameStepManager.canMove()) { + if (!canMove) { velocity.current.set(0, 0, 0); camera.position.copy(capsule.current.end); return;