diff --git a/GAME_FLOW.md b/GAME_FLOW.md new file mode 100644 index 0000000..ed68f79 --- /dev/null +++ b/GAME_FLOW.md @@ -0,0 +1,187 @@ +# Game Flow - La Fabrik + +## Étapes du jeu + +``` +intro → start-intro → naming → bienvenue → star-move → mission2 → searching_problem → preparation → outOfFabrik +``` + +--- + +## Détail des étapes + +### 1. `intro` (initial) + +- État initial au chargement du jeu +- Aucune action, juste une étape de départ +- Transition automatique vers `start-intro` + +### 2. `start-intro` + +- **Déclenchement** : Auto-transition depuis `intro` quand la scène est chargée +- **Action** : Joue l'audio d'intro (`intro`) +- **Attente** : Attend que l'audio se termine +- **Transition** : Vers `naming` quand l'audio se termine + +### 3. `naming` + +- **Déclenchement** : Quand l'audio d'intro se termine +- **Action** : Affiche un input pour demander le prénom du joueur +- **Attente** : L'utilisateur entre son prénom et valide +- **Transition** : Vers `bienvenue` quand l'utilisateur valide + +### 4. `bienvenue` + +- **Déclenchement** : Quand l'utilisateur valide son prénom +- **Actions** : + - Affiche "Bienvenue {prénom} !" à l'écran + - Joue l'audio de bienvenue +- **Attente** : Attend que l'audio se termine +- **Transition** : Vers `star-move` quand l'audio se termine + +### 5. `star-move` + +- **Déclenchement** : Quand l'audio de bienvenue se termine +- **Action** : Active le mouvement du joueur (`setCanMove(true)`) +- **État** : Le joueur peut maintenant se déplacer librement +- **Zone** : La détection de zone devient active (ZoneDetection) + +### 6. `mission2` + +- **Déclenchement** : Quand le joueur entre dans la zone `fabrikExit` (position: `[-5, 25, -15]`) +- **Actions** : + - Stocke `activityCity: false` dans le store Zustand + - Joue l'audio `alertCentral` +- **État** : Les objets avec hook `useActivityCity()` détectent le changement et jouent leurs animations +- **Attente** : Le joueur atteint la zone de trigger pour `searching_problem` + +### 7. `searching_problem` + +- **Déclenchement** : Quand le joueur entre dans la zone `searchingProblemZone` (position: `[-5, 25, -30]`) +- **Actions** : + - Joue l'audio `searchingProblem` + - Affiche l'objet "central" (position: `[1, 15, -45]`) +- **Attente** : Le joueur interagit avec l'objet "central" + +### 8. `preparation` + +- **Déclenchement** : Quand le joueur interagit avec l'objet "central" +- **Actions** : + - Bloque le mouvement (`setCanMove(false)`) + - Cache l'objet "central" + +### 9. `outOfFabrik` + +- **Déclenchement** : (non implémenté pour le moment) +- **Action** : Transition vers l'étape finale + +--- + +## Fichiers clés + +| Fichier | Rôle | +| --------------------------------------- | --------------------------------------------------------- | +| `src/stores/gameStore.ts` | Store Zustand pour l'état global du jeu | +| `src/stateManager/GameStepManager.ts` | Synchronise avec le store Zustand | +| `src/components/game/GameFlow.tsx` | Gère les transitions automatiques et la lecture audio | +| `src/components/ui/IntroUI.tsx` | Affiche l'input pour le prénom et le message de bienvenue | +| `src/components/zone/ZoneDetection.tsx` | Détecte quand le joueur entre dans une zone | +| `src/components/3d/CentralObject.tsx` | Objet interactif "central" pour la mission 2 | +| `src/data/audioConfig.ts` | Chemins des fichiers audio | +| `src/data/zones.ts` | Configuration des zones de transition | +| `src/hooks/useActivityCity.ts` | Hook pour détecter le changement d'activité de la ville | + +--- + +## Configuration audio + +```typescript +// src/data/audioConfig.ts +export const AUDIO_PATHS = { + intro: "/sounds/fa.mp3", + bienvenue: "/sounds/fa.mp3", + alertCentral: "/sounds/fa.mp3", + searchingProblem: "/sounds/fa.mp3", +}; +``` + +--- + +## Configuration des zones + +```typescript +// src/data/zones.ts +export const ZONES: Zone[] = [ + { + id: "fabrikExit", + position: [-5, 25, -15], + radius: 10, + height: 20, + targetStep: "mission2", + }, + { + id: "searchingProblemZone", + position: [-5, 25, -30], + radius: 10, + height: 20, + targetStep: "searching_problem", + }, +]; +``` + +--- + +## Store Zustand + +```typescript +// src/stores/gameStore.ts +interface GameState { + step: GameStep; + activityCity: boolean; + playerName: string; + canMove: boolean; + setStep: (step: GameStep) => void; + setActivityCity: (value: boolean) => void; + setPlayerName: (name: string) => void; + setCanMove: (canMove: boolean) => void; +} +``` + +--- + +## Hooks personnalisés + +### useActivityCity + +Permet aux objets 3D de réagir au changement d'activité de la ville : + +```typescript +import { useActivityCity } from "@/hooks/useActivityCity"; + +function MyAnimatedObject() { + const activityCity = useActivityCity(); // true par défaut, false en mission2 + + // L'animation se déclenche quand activityCity change à false + // Utiliser useEffect pour réagir au changement +} +``` + +--- + +## Debug + +En mode debug (`?debug` dans l'URL), on peut voir : + +- **Game Step** : L'étape actuelle dans le panneau lil-gui +- **Player Position** : Position X, Y, Z du joueur en temps réel +- **Zone Visualization** : Anneaux visuels au sol pour les zones + cylindres transparents + +--- + +## Notes techniques + +- Le mouvement du joueur est bloqué tant que `canMove` est `false` +- Le store Zustand (`useGameStore`) est la source principale de vérité +- `GameStepManager` synchronise automatiquement avec le store Zustand lors des transitions +- Les transitions via les zones utilisent `GameStepManager.transitionTo()` qui met à jour le store +- L'audio utilise un callback `onEnded` pour déclencher les transitions automatiques diff --git a/docs/technical/game-flow.md b/docs/technical/game-flow.md new file mode 100644 index 0000000..ed68f79 --- /dev/null +++ b/docs/technical/game-flow.md @@ -0,0 +1,187 @@ +# Game Flow - La Fabrik + +## Étapes du jeu + +``` +intro → start-intro → naming → bienvenue → star-move → mission2 → searching_problem → preparation → outOfFabrik +``` + +--- + +## Détail des étapes + +### 1. `intro` (initial) + +- État initial au chargement du jeu +- Aucune action, juste une étape de départ +- Transition automatique vers `start-intro` + +### 2. `start-intro` + +- **Déclenchement** : Auto-transition depuis `intro` quand la scène est chargée +- **Action** : Joue l'audio d'intro (`intro`) +- **Attente** : Attend que l'audio se termine +- **Transition** : Vers `naming` quand l'audio se termine + +### 3. `naming` + +- **Déclenchement** : Quand l'audio d'intro se termine +- **Action** : Affiche un input pour demander le prénom du joueur +- **Attente** : L'utilisateur entre son prénom et valide +- **Transition** : Vers `bienvenue` quand l'utilisateur valide + +### 4. `bienvenue` + +- **Déclenchement** : Quand l'utilisateur valide son prénom +- **Actions** : + - Affiche "Bienvenue {prénom} !" à l'écran + - Joue l'audio de bienvenue +- **Attente** : Attend que l'audio se termine +- **Transition** : Vers `star-move` quand l'audio se termine + +### 5. `star-move` + +- **Déclenchement** : Quand l'audio de bienvenue se termine +- **Action** : Active le mouvement du joueur (`setCanMove(true)`) +- **État** : Le joueur peut maintenant se déplacer librement +- **Zone** : La détection de zone devient active (ZoneDetection) + +### 6. `mission2` + +- **Déclenchement** : Quand le joueur entre dans la zone `fabrikExit` (position: `[-5, 25, -15]`) +- **Actions** : + - Stocke `activityCity: false` dans le store Zustand + - Joue l'audio `alertCentral` +- **État** : Les objets avec hook `useActivityCity()` détectent le changement et jouent leurs animations +- **Attente** : Le joueur atteint la zone de trigger pour `searching_problem` + +### 7. `searching_problem` + +- **Déclenchement** : Quand le joueur entre dans la zone `searchingProblemZone` (position: `[-5, 25, -30]`) +- **Actions** : + - Joue l'audio `searchingProblem` + - Affiche l'objet "central" (position: `[1, 15, -45]`) +- **Attente** : Le joueur interagit avec l'objet "central" + +### 8. `preparation` + +- **Déclenchement** : Quand le joueur interagit avec l'objet "central" +- **Actions** : + - Bloque le mouvement (`setCanMove(false)`) + - Cache l'objet "central" + +### 9. `outOfFabrik` + +- **Déclenchement** : (non implémenté pour le moment) +- **Action** : Transition vers l'étape finale + +--- + +## Fichiers clés + +| Fichier | Rôle | +| --------------------------------------- | --------------------------------------------------------- | +| `src/stores/gameStore.ts` | Store Zustand pour l'état global du jeu | +| `src/stateManager/GameStepManager.ts` | Synchronise avec le store Zustand | +| `src/components/game/GameFlow.tsx` | Gère les transitions automatiques et la lecture audio | +| `src/components/ui/IntroUI.tsx` | Affiche l'input pour le prénom et le message de bienvenue | +| `src/components/zone/ZoneDetection.tsx` | Détecte quand le joueur entre dans une zone | +| `src/components/3d/CentralObject.tsx` | Objet interactif "central" pour la mission 2 | +| `src/data/audioConfig.ts` | Chemins des fichiers audio | +| `src/data/zones.ts` | Configuration des zones de transition | +| `src/hooks/useActivityCity.ts` | Hook pour détecter le changement d'activité de la ville | + +--- + +## Configuration audio + +```typescript +// src/data/audioConfig.ts +export const AUDIO_PATHS = { + intro: "/sounds/fa.mp3", + bienvenue: "/sounds/fa.mp3", + alertCentral: "/sounds/fa.mp3", + searchingProblem: "/sounds/fa.mp3", +}; +``` + +--- + +## Configuration des zones + +```typescript +// src/data/zones.ts +export const ZONES: Zone[] = [ + { + id: "fabrikExit", + position: [-5, 25, -15], + radius: 10, + height: 20, + targetStep: "mission2", + }, + { + id: "searchingProblemZone", + position: [-5, 25, -30], + radius: 10, + height: 20, + targetStep: "searching_problem", + }, +]; +``` + +--- + +## Store Zustand + +```typescript +// src/stores/gameStore.ts +interface GameState { + step: GameStep; + activityCity: boolean; + playerName: string; + canMove: boolean; + setStep: (step: GameStep) => void; + setActivityCity: (value: boolean) => void; + setPlayerName: (name: string) => void; + setCanMove: (canMove: boolean) => void; +} +``` + +--- + +## Hooks personnalisés + +### useActivityCity + +Permet aux objets 3D de réagir au changement d'activité de la ville : + +```typescript +import { useActivityCity } from "@/hooks/useActivityCity"; + +function MyAnimatedObject() { + const activityCity = useActivityCity(); // true par défaut, false en mission2 + + // L'animation se déclenche quand activityCity change à false + // Utiliser useEffect pour réagir au changement +} +``` + +--- + +## Debug + +En mode debug (`?debug` dans l'URL), on peut voir : + +- **Game Step** : L'étape actuelle dans le panneau lil-gui +- **Player Position** : Position X, Y, Z du joueur en temps réel +- **Zone Visualization** : Anneaux visuels au sol pour les zones + cylindres transparents + +--- + +## Notes techniques + +- Le mouvement du joueur est bloqué tant que `canMove` est `false` +- Le store Zustand (`useGameStore`) est la source principale de vérité +- `GameStepManager` synchronise automatiquement avec le store Zustand lors des transitions +- Les transitions via les zones utilisent `GameStepManager.transitionTo()` qui met à jour le store +- L'audio utilise un callback `onEnded` pour déclencher les transitions automatiques diff --git a/docs/technical/mission-flow.md b/docs/technical/mission-flow.md new file mode 100644 index 0000000..4c2f766 --- /dev/null +++ b/docs/technical/mission-flow.md @@ -0,0 +1,79 @@ +# Mission Flow + +This document describes the mission intro and mission 2 prototype flow after it was merged into the current architecture. + +## Source Of Truth + +Mission flow state lives in the global game store: + +```txt +src/managers/stores/useGameStore.ts +``` + +The store owns the `missionFlow` slice: + +```ts +missionFlow: { + step: GameStep; + activityCity: boolean; + playerName: string; + canMove: boolean; + dialogMessage: string | null; +} +``` + +This keeps global gameplay state in Zustand instead of splitting it across a separate mission store or a gameplay manager. + +## Managers Boundary + +Managers stay responsible for local runtime services: + +- `AudioManager` owns audio elements, audio pools, music playback, category volume, and stereo pan. +- `InteractionManager` owns transient focused/nearby/held interaction handles. + +Mission progression is not owned by a manager. Components update the store through explicit actions such as `setFlowStep`, `setCanMove`, `showDialog`, and `hideDialog`. + +## Runtime Components + +- `src/components/game/GameFlow.tsx` reacts to `missionFlow.step` and triggers one-off side effects such as intro audio and movement unlocks. +- `src/components/zone/ZoneDetection.tsx` reads the camera position and moves the flow to a target step when the player enters a configured zone. +- `src/components/three/interaction/CentralObject.tsx` and `VillageoisHelperObject.tsx` expose temporary interactive mission objects. +- `src/pages/page.tsx` mounts mission HTML overlays: `IntroUI`, `BienvenueDisplay`, and `DialogMessage`. +- `src/world/player/PlayerController.tsx` reads `missionFlow.canMove` as an additional movement lock. + +## Step Sequence + +The prototype currently uses these steps: + +```ts +"intro" | + "start-intro" | + "naming" | + "bienvenue" | + "star-move" | + "mission2" | + "searching" | + "helped" | + "manipulation" | + "outOfFabrik"; +``` + +These steps are mission-flow prototype states. They do not replace `mainState` or the repair mission step machine used by `RepairGame`. + +## Zone Configuration + +Zone triggers live in: + +```txt +src/data/zones.ts +``` + +Each zone has an id, position, radius, height, and `targetStep`. `ZoneDetection` marks a zone as triggered after the first activation so the same zone does not replay its transition every frame. + +## Rules + +- Keep mission flow state in `useGameStore.missionFlow`. +- Do not reintroduce `GameStepManager` for global state transitions. +- Do not create a second Zustand store for mission flow unless the state becomes independent from game progression. +- Keep side effects such as audio playback in components or service managers, but keep the state transition itself in the store. +- Keep per-frame values such as camera position and zone distance checks out of Zustand. diff --git a/src/components/game/GameFlow.tsx b/src/components/game/GameFlow.tsx new file mode 100644 index 0000000..1434ac7 --- /dev/null +++ b/src/components/game/GameFlow.tsx @@ -0,0 +1,64 @@ +import { useEffect, useRef } from "react"; +import { AudioManager } from "@/managers/AudioManager"; +import { useGameStore } from "@/managers/stores/useGameStore"; +import { AUDIO_PATHS } from "@/data/audioConfig"; + +export function GameFlow(): null { + const step = useGameStore((state) => state.intro.currentStep); + const setStep = useGameStore((state) => state.setIntroStep); + const setActivityCity = useGameStore((state) => state.setActivityCity); + const setCanMove = useGameStore((state) => state.setCanMove); + const hasInitialized = useRef(false); + + useEffect(() => { + if (!hasInitialized.current && step === "intro") { + hasInitialized.current = true; + setStep("start-intro"); + } + }, [step, setStep]); + + useEffect(() => { + if (step === "start-intro") { + const audio = AudioManager.getInstance(); + audio.playSoundWithCallback(AUDIO_PATHS.intro, 0.5, () => { + setStep("naming"); + }); + + return () => {}; + } + + if (step === "bienvenue") { + const audio = AudioManager.getInstance(); + audio.playSoundWithCallback(AUDIO_PATHS.bienvenue, 0.5, () => { + setCanMove(true); + setStep("star-move"); + }); + + return () => {}; + } + + if (step === "mission2") { + setActivityCity(false); + const audio = AudioManager.getInstance(); + audio.playSound(AUDIO_PATHS.alertCentral, 0.5); + } + + if (step === "searching") { + const audio = AudioManager.getInstance(); + audio.playSound(AUDIO_PATHS.searching, 0.5); + } + + if (step === "helped") { + const audio = AudioManager.getInstance(); + audio.playSound(AUDIO_PATHS.helped, 0.5); + } + + if (step === "manipulation") { + setCanMove(false); + } + + return undefined; + }, [step, setStep, setActivityCity, setCanMove]); + + return null; +} diff --git a/src/components/three/interaction/NPCHelper.tsx b/src/components/three/interaction/NPCHelper.tsx new file mode 100644 index 0000000..e89e981 --- /dev/null +++ b/src/components/three/interaction/NPCHelper.tsx @@ -0,0 +1,42 @@ +import { InteractableObject } from "@/components/three/interaction/InteractableObject"; +import { useGameStore } from "@/managers/stores/useGameStore"; +import { Debug } from "@/utils/debug/Debug"; +import type { Vector3Tuple } from "@/types/three/three"; + +interface NPCHelperProps { + position: Vector3Tuple; +} + +export function NPCHelper({ position }: NPCHelperProps): React.JSX.Element { + const step = useGameStore((state) => state.intro.currentStep); + const setStep = useGameStore((state) => state.setIntroStep); + const debug = Debug.getInstance(); + + const handlePress = (): void => { + if (step === "searching") { + setStep("helped"); + } + }; + + const shouldShow = step === "searching" || debug.active; + + if (!shouldShow) { + return <>; + } + + return ( + + + + + + + + + ); +} diff --git a/src/components/three/interaction/PyloneDestroyed.tsx b/src/components/three/interaction/PyloneDestroyed.tsx new file mode 100644 index 0000000..f7c567c --- /dev/null +++ b/src/components/three/interaction/PyloneDestroyed.tsx @@ -0,0 +1,52 @@ +import { InteractableObject } from "@/components/three/interaction/InteractableObject"; +import { useGameStore } from "@/managers/stores/useGameStore"; +import { Debug } from "@/utils/debug/Debug"; +import type { Vector3Tuple } from "@/types/three/three"; + +interface PyloneDestroyedProps { + position: Vector3Tuple; +} + +export function PyloneDestroyed({ + position, +}: PyloneDestroyedProps): React.JSX.Element { + const step = useGameStore((state) => state.intro.currentStep); + const setStep = useGameStore((state) => state.setIntroStep); + const setCanMove = useGameStore((state) => state.setCanMove); + const showDialog = useGameStore((state) => state.showDialog); + const debug = Debug.getInstance(); + + const handlePress = (): void => { + if (step === "helped") { + setCanMove(false); + setStep("manipulation"); + } else if (step === "searching") { + showDialog( + "Cet objet est trop lourd pour le porter tout seul, trouve de l'aide", + ); + } + }; + + const shouldShow = + step === "helped" || step === "manipulation" || debug.active; + + if (!shouldShow) { + return <>; + } + + return ( + + + + + + + + + ); +} 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 new file mode 100644 index 0000000..45ace4e --- /dev/null +++ b/src/components/ui/IntroUI.tsx @@ -0,0 +1,129 @@ +import { useState } from "react"; +import { useGameStore } from "@/managers/stores/useGameStore"; + +export function IntroUI(): React.JSX.Element | null { + const step = useGameStore((state) => state.intro.currentStep); + const setPlayerName = useGameStore((state) => state.setPlayerName); + const setStep = useGameStore((state) => state.setIntroStep); + const [inputValue, setInputValue] = useState(""); + + if (step !== "naming") return null; + + const handleSubmit = (): void => { + if (inputValue.trim() === "") return; + + setPlayerName(inputValue.trim()); + setStep("bienvenue"); + }; + + const handleKeyDown = (e: React.KeyboardEvent): void => { + if (e.key === "Enter") { + handleSubmit(); + } + }; + + return ( +
+
+

+ Quel est votre prenom ? +

+ setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Votre prenom" + autoFocus + style={{ + padding: "0.75rem", + fontSize: "1rem", + borderRadius: "6px", + border: "1px solid #444", + backgroundColor: "#2a2a2a", + color: "#fff", + outline: "none", + }} + /> + +
+
+ ); +} + +export function BienvenueDisplay(): React.JSX.Element | null { + const step = useGameStore((state) => state.intro.currentStep); + const playerName = useGameStore((state) => state.missionFlow.playerName); + + if (step !== "bienvenue") return null; + + return ( +
+

+ Bienvenue {playerName} ! +

+
+ ); +} diff --git a/src/components/ui/debug/GameStateDebugPanel.tsx b/src/components/ui/debug/GameStateDebugPanel.tsx index 664153d..7632d6f 100644 --- a/src/components/ui/debug/GameStateDebugPanel.tsx +++ b/src/components/ui/debug/GameStateDebugPanel.tsx @@ -4,6 +4,7 @@ import { useGameStore, } from "@/managers/stores/useGameStore"; import { isMissionStep, MISSION_STEPS } from "@/types/gameplay/repairMission"; +import { GAME_STEPS, type GameStep } from "@/types/game"; const MAIN_STATES: MainGameState[] = [ "intro", @@ -29,7 +30,7 @@ export function GameStateDebugPanel(): React.JSX.Element { const detail = useGameStore((state) => { switch (state.mainState) { case "intro": - return state.intro.hasCompleted ? "completed" : "waiting"; + return state.intro.currentStep; case "bike": return state.bike.currentStep; case "pylone": @@ -41,7 +42,7 @@ export function GameStateDebugPanel(): React.JSX.Element { } }); const setMainState = useGameStore((state) => state.setMainState); - const setIntroState = useGameStore((state) => state.setIntroState); + const setIntroStep = useGameStore((state) => state.setIntroStep); const setBikeState = useGameStore((state) => state.setBikeState); const setPyloneState = useGameStore((state) => state.setPyloneState); const setFermeState = useGameStore((state) => state.setFermeState); @@ -52,14 +53,14 @@ export function GameStateDebugPanel(): React.JSX.Element { const subStateOptions = mainState === "intro" - ? ["waiting", "completed"] + ? GAME_STEPS : mainState === "outro" ? ["waiting", "started"] : MISSION_STEPS; function setSubState(nextSubState: string): void { if (mainState === "intro") { - setIntroState({ hasCompleted: nextSubState === "completed" }); + setIntroStep(nextSubState as GameStep); return; } diff --git a/src/components/zone/ZoneDetection.tsx b/src/components/zone/ZoneDetection.tsx new file mode 100644 index 0000000..16070f6 --- /dev/null +++ b/src/components/zone/ZoneDetection.tsx @@ -0,0 +1,148 @@ +import { useEffect, useRef, useState } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import * as THREE from "three"; +import { ZONES } from "@/data/zones"; +import { useGameStore } from "@/managers/stores/useGameStore"; +import { Debug } from "@/utils/debug/Debug"; +import { GAME_STEPS } from "@/types/game"; + +const _playerPos = new THREE.Vector3(); +const _zonePos = new THREE.Vector3(); + +export function ZoneDetection(): null { + const camera = useThree((state) => state.camera); + const triggeredZones = useRef>(new Set()); + const debug = Debug.getInstance(); + const step = useGameStore((state) => state.intro.currentStep); + const setStep = useGameStore((state) => state.setIntroStep); + + useEffect(() => { + if (!debug.active) return; + + const folder = debug.createFolder("Game"); + if (!folder) return; + + const gameState = { step: step }; + const playerPos = { x: 0, y: 0, z: 0 }; + + 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 unsubStore = useGameStore.subscribe((state) => { + gameState.step = state.intro.currentStep; + folder.controllersRecursive().forEach((c) => c.updateDisplay()); + }); + + let frameId: number; + const updatePlayerPos = (): void => { + camera.getWorldPosition(_playerPos); + playerPos.x = Math.round(_playerPos.x * 100) / 100; + playerPos.y = Math.round(_playerPos.y * 100) / 100; + playerPos.z = Math.round(_playerPos.z * 100) / 100; + folder.controllersRecursive().forEach((c) => c.updateDisplay()); + frameId = requestAnimationFrame(updatePlayerPos); + }; + updatePlayerPos(); + + return () => { + cancelAnimationFrame(frameId); + debug.destroyFolder("Game"); + unsubStore(); + }; + }, [debug, camera, step]); + + useFrame(() => { + camera.getWorldPosition(_playerPos); + + for (const zone of ZONES) { + if (triggeredZones.current.has(zone.id)) continue; + + _zonePos.set(...zone.position); + + const distanceSq = _playerPos.distanceToSquared(_zonePos); + + if (distanceSq <= zone.radius * zone.radius) { + setStep(zone.targetStep); + triggeredZones.current.add(zone.id); + break; + } + } + }); + + return null; +} + +export function ZoneDebugVisuals(): React.JSX.Element | null { + const debug = Debug.getInstance(); + const camera = useThree((state) => state.camera); + const [triggeredZones, setTriggeredZones] = useState>(new Set()); + + useFrame(() => { + camera.getWorldPosition(_playerPos); + + for (const zone of ZONES) { + if (triggeredZones.has(zone.id)) continue; + + _zonePos.set(...zone.position); + + const distanceSq = _playerPos.distanceToSquared(_zonePos); + + if (distanceSq <= zone.radius * zone.radius) { + setTriggeredZones((prev) => new Set(prev).add(zone.id)); + break; + } + } + }); + + if (!debug.active) return null; + + return ( + <> + {ZONES.map((zone) => ( + + ))} + + ); +} + +function ZoneVisual({ + position, + radius, + height, + triggered, +}: { + position: [number, number, number]; + radius: number; + height: number; + triggered: boolean; +}): React.JSX.Element { + const color = triggered ? "#00ff00" : "#ff0000"; + + return ( + + + + + + + + + + + ); +} diff --git a/src/data/audioConfig.ts b/src/data/audioConfig.ts new file mode 100644 index 0000000..6a7c6bc --- /dev/null +++ b/src/data/audioConfig.ts @@ -0,0 +1,7 @@ +export const AUDIO_PATHS = { + intro: "/sounds/effect/fa.mp3", + bienvenue: "/sounds/effect/fa.mp3", + alertCentral: "/sounds/effect/fa.mp3", + searching: "/sounds/effect/fa.mp3", + helped: "/sounds/effect/fa.mp3", +} as const; diff --git a/src/data/docs/docsTranslations.ts b/src/data/docs/docsTranslations.ts new file mode 100644 index 0000000..ce624c9 --- /dev/null +++ b/src/data/docs/docsTranslations.ts @@ -0,0 +1,775 @@ +export const readmeFr = `# La-Fabrik + +Une expérience web 3D interactive pour La Fabrik Durable, un service low-tech de réparation et de transformation situé à Altera, une ville post-capitaliste reconstruite en 2039. Les joueurs incarnent un technicien fraîchement intégré et vivent une journée de service : réparer un vélo électrique, remettre en état un réseau d'énergie et améliorer le système d'irrigation d'une ferme verticale. + +Construit avec React, Three.js et Vite. Fonctionne dans le navigateur, sans installation côté utilisateur. + +## Stack technique + +### Build et langage + +| Package | +| -------------------------------------------------- | +| [TypeScript](https://www.typescriptlang.org/docs/) | +| [React](https://react.dev/learn) | +| [Vite](https://vite.dev/guide/) | +| [ESLint](https://eslint.org/docs/latest/) | +| [Prettier](https://prettier.io/docs/) | + +### Moteur 3D + +| Package | +| ----------------------------------------------------------------------------------------- | +| [Three.js](https://threejs.org/docs/) | +| [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) | +| [@react-three/drei](https://pmndrs.github.io/drei) | +| [@react-three/rapier](https://rapier.rs/docs/) | +| [GSAP](https://gsap.com/docs/v3/Installation/) | + +### Performance et effets + +| Package | +| --------------------------------------------------------------------------- | +| [r3f-perf](https://github.com/utsuboco/r3f-perf) | +| [AnimationMixer](https://threejs.org/docs/#api/en/animation/AnimationMixer) | + +## Structure du projet + +\`\`\` +la-fabrik/ +├── public/ +│ ├── models/ +│ │ ├── map/ # Carte de base, chargée au démarrage +│ │ ├── workshop/ +│ │ ├── powerGrid/ +│ │ └── farm/ +│ ├── textures/ +│ └── sounds/ +│ +└── src/ + ├── world/ # Composition du monde 3D persistant + │ ├── World.tsx # Composition de la scène active + │ ├── GameMap.tsx # Chargement de carte et collision octree + │ ├── Lighting.tsx # Lumières ambiante, directionnelle et ponctuelles + │ ├── Environment.tsx # Arrière-plan et modèle de ciel + │ ├── GameMusic.tsx # Cycle de vie de la musique de jeu + │ ├── debug/ # Scène de test debug + │ └── player/ # Contrôleur joueur et caméra + │ + ├── components/ + │ ├── three/ # Composants R3F par domaine + │ └── ui/ # Overlays HTML hors Canvas + │ + ├── managers/ # Logique, état et orchestration + ├── hooks/ # Hooks React autour des managers + ├── data/ # Configuration statique + ├── shaders/ # Shaders GLSL + └── utils/ # Utilitaires partagés et debug +\`\`\` + +## Démarrage + +\`\`\`bash +git clone https://github.com/La-Fabrik-Durable/La-Fabrik.git +cd La-Fabrik +npm install +npm run dev +\`\`\` + +- application : \`http://localhost:5173\` +- mode debug : \`http://localhost:5173?debug\` + +## Licence + +Voir le fichier [LICENSE](./LICENSE). +`; + +export const architectureFr = `# Architecture actuelle + +Ce document décrit le code réellement présent aujourd'hui dans le dépôt. + +## Structure runtime + +- \`src/App.tsx\` monte le \`RouterProvider\`, qui pilote l'affichage des vues de l'application. +- \`src/pages/page.tsx\` monte le \`Canvas\`, le \`World\` 3D, l'overlay de performance debug et les overlays HTML. +- \`src/world/World.tsx\` compose la scène active avec : + - l'environnement et l'éclairage + - les helpers debug et le mode caméra debug + - soit la carte principale, soit la scène de test physique debug + - le rig joueur quand le mode caméra actif est \`player\` +- \`src/world/GameMap.tsx\` charge les modèles de carte disponibles et construit l'octree de collision. +- \`src/world/GameStageContent.tsx\` est enveloppé dans le contexte Rapier \`Physics\` dans la scène de jeu de production afin que les objets gameplay de stage puissent utiliser la physique sans migrer la carte ou le joueur vers Rapier. Il monte maintenant des instances réutilisables de \`RepairGame\` pour les états de mission \`bike\`, \`pylone\` et \`ferme\`. +- \`src/world/debug/TestMap.tsx\` fournit une carte orientée debug pour les interactions et la physique, avec les objets existants de grab, trigger et preview de modèle, plus des zones playground de réparation séparées \`Bike\`, \`Pylone\` et \`Farm\`. +- \`src/world/player/Player.tsx\` monte la caméra et le contrôleur. +- \`src/world/player/PlayerController.tsx\` gère le mouvement pointer lock, le saut, le verrouillage de déplacement pendant les étapes repair et les inputs d'interaction. + +## Frontières physiques + +Le projet utilise actuellement deux couches de collision avec des responsabilités séparées : + +- \`GameMap\` construit une octree utilisée par le contrôleur joueur pour les collisions avec la carte. +- \`GameStageContent\` est enveloppé dans Rapier \`Physics\` pour les objets gameplay comme les triggers de réparation, les mallettes, les objets saisissables et les futurs objets spécifiques aux missions. +- \`TestMap\` possède son propre playground Rapier \`Physics\` afin de peaufiner le gameplay de réparation par state de mission sans dépendre du placement de la carte de production. + +Le joueur et l'octree de carte doivent rester hors du provider Rapier tant qu'il n'existe pas de plan de migration volontaire. Cela évite de mélanger les règles de déplacement joueur avec la physique d'objets avant que les systèmes gameplay en aient besoin. + +## Modèle d'interaction + +- \`src/managers/InteractionManager.ts\` est la source d'état actuelle des interactions. +- \`src/components/three/interaction/InteractableObject.tsx\` gère la détection de focus par distance et raycasting. +- \`src/components/three/interaction/TriggerObject.tsx\` implémente les interactions de type trigger. +- \`src/components/three/interaction/GrabbableObject.tsx\` implémente les interactions saisir / relâcher. +- \`src/hooks/interaction/useInteraction.ts\` expose un snapshot d'interaction à l'UI React. +- \`src/components/ui/InteractPrompt.tsx\` affiche le prompt \`E\` pour les interactions trigger. + +## Audio + +- \`src/managers/AudioManager.ts\` fournit la lecture de sons one-shot avec pool, la musique en boucle, les volumes par catégorie et un pan stéréo optionnel pour les sons one-shot. +- Les catégories audio supportées sont \`music\`, \`sfx\` et \`dialogue\`. +- Les interactions trigger peuvent lancer directement des SFX via \`AudioManager\`. + +## Menu options + +- \`src/managers/stores/useSettingsStore.ts\` stocke les réglages de volume musique, volume SFX, volume dialogue, sous-titres, langue des sous-titres, runtime de réparation et visibilité du menu. +- \`src/components/ui/GameSettingsMenu.tsx\` rend le menu options en jeu. +- \`src/components/ui/GameUI.tsx\` monte le menu comme overlay HTML hors canvas. +- \`Esc\` ouvre et ferme le menu, et \`src/world/player/PlayerController.tsx\` ignore les inputs joueur pendant son ouverture. +- Les changements de volume sont transmis à \`AudioManager\` par catégorie. + +## Dialogues et sous-titres + +- \`public/sounds/dialogue/dialogues.json\` est le manifeste runtime des dialogues. +- Les fichiers audio de dialogue vivent dans \`public/sounds/dialogue/\`. +- Les fichiers de sous-titres vivent dans \`public/sounds/dialogue/subtitles/{fr|en}/\`. +- Le modèle actuel utilise un fichier SRT par voix et par langue. +- \`src/types/dialogues/dialogues.ts\` contient les types du manifeste. +- \`src/utils/dialogues/dialogueManifestValidation.ts\` valide la forme du manifeste au runtime. +- \`src/utils/dialogues/loadDialogueManifest.ts\` charge le manifeste et les cues SRT, avec fallback français si la langue sélectionnée manque. +- \`src/utils/subtitles/parseSrt.ts\` parse les blocs et timecodes SRT. +- \`src/utils/dialogues/playDialogue.ts\` joue l'audio de dialogue et synchronise le sous-titre actif avec le temps de l'élément audio. +- \`src/managers/stores/useSubtitleStore.ts\` stocke la cue de sous-titre affichée. +- \`src/components/ui/Subtitles.tsx\` rend l'overlay de sous-titres. +- \`src/world/GameDialogues.tsx\` déclenche actuellement les dialogues qui définissent un \`timecode\`. +- La lecture de dialogue est mise en file pour éviter les chevauchements. + +## Cinématiques + +- \`public/cinematics.json\` est le manifeste runtime des cinématiques. +- \`src/types/cinematics/cinematics.ts\` contient les types du manifeste. +- \`src/utils/cinematics/cinematicManifestValidation.ts\` valide la forme du manifeste. +- \`src/utils/cinematics/loadCinematicManifest.ts\` charge \`/cinematics.json\`. +- \`src/world/GameCinematics.tsx\` déclenche les cinématiques qui définissent un \`timecode\` global. +- Les cinématiques utilisent GSAP pour animer la position caméra et sa cible de regard. +- Les \`dialogueCues\` d'une cinématique déclenchent des dialogues à des temps relatifs au début de la cinématique. +- \`useGameStore.isCinematicPlaying\` sert à bloquer les inputs joueur pendant une cinématique. + +## Système debug + +- Le mode debug est activé avec \`?debug\`. +- \`src/utils/debug/Debug.ts\` possède l'instance \`lil-gui\` et les contrôles debug. +- \`src/hooks/debug/useCameraMode.ts\` et \`src/hooks/debug/useSceneMode.ts\` s'abonnent à l'état debug. +- \`src/components/debug/DebugPerf.tsx\` monte \`r3f-perf\` en lazy uniquement en mode debug. +- \`src/components/ui/debug/DebugOverlayLayout.tsx\` monte l'overlay HTML debug compact quand il est activé depuis \`lil-gui\`. +- \`src/components/ui/debug/GameStateDebugPanel.tsx\` expose l'état de jeu courant, le changement de main/sub-state, les contrôles previous/next step et le reset. +- \`src/components/ui/debug/HandTrackingDebugPanel.tsx\` affiche le statut hand tracking, l'usage, le modèle de gant chargé, le nombre de mains et l'état fist pendant l'activation du hand tracking. +- \`src/components/three/handTracking/HandTrackingGlove.tsx\` place les modèles riggés \`gant_l\` et \`gant_r\` sur les mains détectées dans la scène physics debug. +- \`src/components/debug/scene/DebugHelpers.tsx\` monte les helpers debug. +- \`src/components/debug/scene/DebugCameraControls.tsx\` monte la caméra libre debug. +- Les contrôles globaux \`lil-gui\` incluent camera mode, scene mode, \`R3F Perf\` et \`Debug Overlay\`; les contrôles d'interaction vivent dans le dossier \`Interaction\`. + +## Domaines de composants 3D + +- \`src/components/three/models/\` contient les helpers de modèles réutilisables comme \`ExplodableModel\`. +- \`src/components/three/interaction/\` contient les wrappers d'interaction réutilisables comme \`InteractableObject\`, \`TriggerObject\` et \`GrabbableObject\`. +- \`src/components/three/handTracking/\` contient les modèles debug R3F liés au hand tracking, comme les gants. +- \`src/components/three/gameplay/\` contient les composants de gameplay de réparation : le flow de production réutilisable \`RepairGame\`, la mallette, les étapes de réparation et les prompts. +- \`src/components/three/world/\` contient les objets world/environnement réutilisables comme \`SkyModel\`. + +## Limites actuelles + +- Le dépôt est encore un prototype, pas le runtime complet du jeu. +- \`src/world/debug/TestMap.tsx\` fait encore partie de la composition active. +- Il n'existe pas encore d'orchestrateur gameplay central comme \`GameManager\`. +- L'état de mission existe dans Zustand et le flow de réparation est implémenté comme prototype pour les missions de réparation actuelles. +- Les cinématiques et dialogues existent comme systèmes prototype pilotés par timecode; les branches de dialogue et l'orchestration gameplay globale restent limitées. +- Le joueur utilise une collision octree et des règles simples, pas une pile physique gameplay complète. +`; + +export const targetArchitectureFr = `# Architecture cible + +Ce document décrit l'architecture visée à moyen terme pour le projet. + +## Relation avec le code actuel + +- \`docs/technical/architecture.md\` reste la source de vérité de ce qui existe maintenant. +- Ce document décrit une direction d'architecture, pas un comportement implémenté. +- Si ce document contredit l'implémentation actuelle, l'implémentation actuelle gagne. + +## Objectifs + +- Garder \`App.tsx\` petit et centré sur l'orchestration. +- Séparer le code de production du monde des chemins runtime uniquement debug. +- Garder une source de vérité claire par responsabilité. +- Faire grandir les systèmes gameplay progressivement, sans préconstruire une architecture vide. + +## Couches prévues + +### Couche App + +- \`App.tsx\` monte la scène canvas et les overlays HTML de premier niveau. +- Il doit rester fin et éviter la logique gameplay. + +### Couche World + +- \`src/world/\` doit contenir la composition de scène de production et les objets de scène de production. +- Responsabilités attendues : + - composition du monde + - carte, environnement, éclairage + - contrôleur joueur + - ancres d'interaction de production + - post-processing de production si nécessaire + +### Couche Debug + +- Les scènes et outils uniquement debug doivent être isolés du chemin de production. +- Responsabilités attendues : + - \`lil-gui\` + - overlay de performance + - helpers de scène + - caméra libre et contrôles de calibration + - scènes temporaires de test utilisées pendant le développement + +### Couche UI + +- \`src/components/ui/\` doit contenir les overlays HTML visibles par le joueur. +- Exemples futurs : + - crosshair + - flow de chargement + - HUD de mission + - overlays narratifs + +### Couche Gameplay + +- À mesure que le projet grandit, l'état gameplay peut évoluer vers une couche d'orchestration plus claire. +- Sujets probables : + - missions + - zones + - cinématiques + - dialogues + - audio + - interactions + +## Règles + +- Préférer du code direct et fonctionnel plutôt qu'un échafaudage spéculatif. +- Les types partagés doivent rester proches de leur domaine jusqu'à avoir plusieurs vrais consommateurs. +- Éviter de créer de nouveaux managers ou services sans besoin runtime actif. +- 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 +- \`missionFlow\` : état prototype de l'intro et de la mission 2 +- \`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 + +Le slice \`missionFlow\` contient l'étape prototype, le prénom joueur, le lock de déplacement, le flag d'activité de la ville et le message de dialogue temporaire. Il vit dans le store principal parce qu'il s'agit d'un état gameplay global utilisé par l'UI, le world et le controller joueur. + +Les étapes de mission utilisent actuellement cette séquence : + +\`\`\`ts +"locked" | "waiting" | "inspected" | "fragmented" | "scanning" | "repairing" | "reassembling" | "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\`. + +Le gameplay de mission qui peut cibler \`bike\`, \`pylone\` ou \`ferme\` doit préférer les actions génériques de mission : + +\`\`\`ts +const setMissionStep = useGameStore((state) => state.setMissionStep); +const completeMission = useGameStore((state) => state.completeMission); + +setMissionStep("bike", "inspected"); +completeMission("bike"); +\`\`\` + +Cela évite aux composants gameplay réutilisables, comme les flows de réparation, de dupliquer des branches spécifiques à chaque mission avec \`setBikeState\`, \`setPyloneState\` et \`setFermeState\`. + +## Intégration avec le World + +\`src/world/GameStageContent.tsx\` s'abonne à \`mainState\` et monte le contenu spécifique au state courant. + +Pour les missions de réparation, il monte le composant réutilisable \`RepairGame\` avec un id de mission : + +\`\`\`tsx + +\`\`\` + +\`RepairGame\` lit l'étape de mission active depuis le store et écrit les transitions via des actions génériques comme \`setMissionStep\` et \`completeMission\`. Les ids de mission, étapes de mission et guards partagés vivent dans \`src/types/gameplay/repairMission.ts\`, ce qui évite à la configuration statique des missions de dépendre du store Zustand. Le flow de réparation de production supporte actuellement les transitions \`waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission\`. + +Le flow prototype intro et mission 2 est documenté séparément dans \`docs/technical/mission-flow.md\`. Il utilise volontairement la même source de vérité \`useGameStore\`, sans \`GameStepManager\` dédié ni second store Zustand. + +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 : + +- \`DebugOverlayLayout\` : layout compact des panels debug HTML visible avec \`?debug\` +- \`GameStateDebugPanel\` : panneau de progression debug pour consulter/changer le main state, le sub state, avancer/reculer et reset le store +- \`Crosshair\` : aide de visée joueur +- \`InteractPrompt\` : prompt d'interaction +- \`RepairMovementLockIndicator\` : indicateur joueur affiché quand les étapes repair désactivent temporairement le déplacement +- Les overlays du flow mission comme \`IntroUI\`, \`BienvenueDisplay\` et \`DialogMessage\` sont montés par \`src/pages/page.tsx\`, car ce sont des overlays HTML de route plutôt qu'un HUD de jeu persistant. + +\`src/pages/page.tsx\` doit rester fin et monter le canvas, le \`GameUI\` persistant et les overlays de route. + +## 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 + +Déplacer la validation de réparation dans les données de mission lorsque chaque mission aura ses propres nodes de modules cassés, assets de remplacement et événements de complétion. +`; + +export const missionFlowFr = `# Flow de mission + +Ce document décrit le flow prototype d'intro et de mission 2 après son intégration dans l'architecture actuelle. + +## Source de vérité + +L'état du flow de mission vit dans le store global du jeu : + +\`\`\`txt +src/managers/stores/useGameStore.ts +\`\`\` + +Le store possède le slice \`missionFlow\` : + +\`\`\`ts +missionFlow: { + step: GameStep; + activityCity: boolean; + playerName: string; + canMove: boolean; + dialogMessage: string | null; +} +\`\`\` + +Cela garde l'état gameplay global dans Zustand, au lieu de le répartir entre un store mission séparé ou un manager gameplay. + +## Frontière des managers + +Les managers restent responsables de services runtime locaux : + +- \`AudioManager\` possède les éléments audio, les pools audio, la musique, le volume par catégorie et le pan stéréo. +- \`InteractionManager\` possède les handles d'interaction transitoires, focus, nearby et held. + +La progression de mission n'est pas possédée par un manager. Les composants mettent à jour le store via des actions explicites comme \`setFlowStep\`, \`setCanMove\`, \`showDialog\` et \`hideDialog\`. + +## Composants runtime + +- \`src/components/game/GameFlow.tsx\` réagit à \`missionFlow.step\` et déclenche les effets ponctuels comme l'audio d'intro et le déblocage du mouvement. +- \`src/components/zone/ZoneDetection.tsx\` lit la position caméra et fait passer le flow à une étape cible quand le joueur entre dans une zone configurée. +- \`src/components/three/interaction/CentralObject.tsx\` et \`VillageoisHelperObject.tsx\` exposent les objets interactifs temporaires de mission. +- \`src/pages/page.tsx\` monte les overlays HTML de mission : \`IntroUI\`, \`BienvenueDisplay\` et \`DialogMessage\`. +- \`src/world/player/PlayerController.tsx\` lit \`missionFlow.canMove\` comme lock de déplacement supplémentaire. + +## Séquence d'étapes + +Le prototype utilise actuellement ces étapes : + +\`\`\`ts +"intro" | "start-intro" | "naming" | "bienvenue" | "star-move" | "mission2" | "searching" | "helped" | "manipulation" | "outOfFabrik" +\`\`\` + +Ces étapes sont propres au prototype de flow mission. Elles ne remplacent pas \`mainState\` ni la machine d'étapes repair utilisée par \`RepairGame\`. + +## Configuration des zones + +Les triggers de zones vivent dans : + +\`\`\`txt +src/data/zones.ts +\`\`\` + +Chaque zone possède un id, une position, un rayon, une hauteur et un \`targetStep\`. \`ZoneDetection\` marque une zone comme déclenchée après sa première activation pour éviter de rejouer la même transition à chaque frame. + +## Règles + +- Garder l'état du flow mission dans \`useGameStore.missionFlow\`. +- Ne pas réintroduire de \`GameStepManager\` pour les transitions globales. +- Ne pas créer un second store Zustand pour le flow mission sauf si cet état devient réellement indépendant de la progression du jeu. +- Garder les effets de bord comme l'audio dans les composants ou les managers de service, mais garder la transition d'état dans le store. +- Ne pas mettre les valeurs par-frame comme la position caméra ou les distances de zones dans Zustand. +`; + +export const featuresFr = `# Fonctionnalités implémentées + +Ce document liste les fonctionnalités présentes dans le code actuel. + +## Scène + +- Scène React Three Fiber plein écran +- Carte principale chargée depuis \`public/models/{name}/model.glb\`, avec fallback vers \`model.gltf\` +- Scène de test physique debug sélectionnable depuis le panneau debug, avec tests grab/trigger, preview de modèle animé et zones playground de réparation séparées pour \`bike\`, \`pylone\` et \`ferme\` +- Contexte physique Rapier disponible pour les objets gameplay de stage en production +- Éclairage ambiant et directionnel +- Configuration de l'environnement de fond + +## Joueur + +- Mode caméra joueur +- Orientation souris avec pointer lock +- Déplacement avec \`ZQSD\` +- Saut +- Verrouillage du déplacement pendant les étapes repair actives, avec indicateur à l'écran tout en gardant les interactions trigger disponibles +- Collision basée sur une octree contre la carte chargée + +## Interactions + +- Détection de focus par distance et raycast +- Interactions trigger activées avec \`E\` +- Interactions grab activées avec le bouton principal de la souris +- Les objets gameplay avec physique peuvent être montés dans le contenu de stage sans remplacer la collision octree du joueur +- Prompt d'interaction affiché pour les interactions trigger + +## Gameplay de réparation + +- \`RepairGame\` de production réutilisable monté pour les états de mission \`bike\`, \`pylone\` et \`ferme\` +- Le playground physics debug monte le même \`RepairGame\` réutilisable dans des zones \`Bike\`, \`Pylone\` et \`Farm\`, afin de peaufiner chaque state avec un placement isolé avant déplacement vers la carte de production +- Configuration de mission partagée via \`src/data/gameplay/repairMissions.ts\`, avec nodes cassés, placeholders cibles, timing de scan et timing de réassemblage propres à chaque mission +- Flow repair-game avec \`waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission\`, prompts \`.webm\`, apparition/ouverture/sortie de la mallette, vue focalisée de la mallette, indicateur de verrouillage de déplacement pendant la réparation active, interaction trigger sur la mallette, traverse des placeholders de mallette, placement avec snap vers placeholder, feedback de dépôt des pièces cassées, touche \`E\`, hold deux poings, transition de modèle explosé, réassemblage inverse avec particules, scan visuel par pièce, marqueur rouge persistant et vidéo UI centrée sur les pièces cassées, plusieurs choix de pièces grabbables, feedback de validation de la bonne pièce et complétion de mission + +## Audio + +- Volumes par catégorie pour la musique, les SFX et les dialogues +- Lecture de musique en boucle via \`AudioManager\` +- Lecture de sons one-shot pour les SFX et les dialogues, avec pool simple par son +- Pan stéréo optionnel pour les sons one-shot + +## Dialogues et sous-titres + +- Manifeste de dialogues dans \`public/sounds/dialogue/dialogues.json\` +- Audios de dialogue chargés depuis \`public/sounds/dialogue/\` +- Un fichier SRT par voix et par langue +- Fallback vers les sous-titres français quand le fichier de langue sélectionné manque +- Overlay de sous-titres runtime avec couleurs par speaker +- Déclenchement timecodé pour les dialogues qui définissent \`timecode\` +- File d'attente pour éviter les dialogues superposés + +## Cinématiques + +- Manifeste de cinématiques dans \`public/cinematics.json\` +- Déclenchement timecodé des cinématiques +- Lecture de keyframes caméra via GSAP +- Dialogue cues optionnelles synchronisées avec les timelines de cinématique +- Blocage des inputs joueur pendant une cinématique + +## Menu options + +- \`Esc\` ouvre et ferme le menu options en jeu +- Sliders de volume musique, SFX et dialogue +- Toggle d'affichage des sous-titres +- Choix de langue des sous-titres entre français et anglais +- Choix du runtime de réparation entre JavaScript local et serveur Python +- Action quitter qui nettoie les cookies accessibles au navigateur et retourne vers \`/\` + +## Outils debug + +- Le paramètre \`?debug\` active le panneau debug +- Contrôles \`lil-gui\` pour le mode caméra, le mode scène, \`R3F Perf\`, \`Debug Overlay\` et le tuning d'interaction +- Overlay debug compact pour les contrôles de game state et le statut hand tracking +- Le changement de mission dans le panneau game-state debug déverrouille les missions repair encore \`locked\` à \`waiting\` pour accélérer les tests +- Helpers de scène debug +- Caméra libre debug +- Overlay \`r3f-perf\` + +## Éditeur de carte + +- Route \`/editor\` pour inspecter et éditer \`public/map.json\` +- Chargement automatique de \`public/map.json\` quand il existe +- Rendu des modèles disponibles depuis \`public/models/{name}/model.glb\` ou \`model.gltf\` +- Cubes de fallback pour les nodes dont le modèle manque +- Sélection d'objet au clic +- Modes de transformation translation, rotation et scale +- Export JSON pour télécharger la carte modifiée +- Endpoint de sauvegarde dev-server pour écrire \`public/map.json\` +- Éditeur SRT pour les sous-titres de dialogue +- Preview audio et outils de timing pour les cues SRT +- Endpoint de sauvegarde dev-server pour les fichiers SRT +- Validation du manifeste de dialogues depuis l'UI de l'éditeur +- Éditeur de manifeste dialogues avec preview et création assistée de cue SRT FR +- Éditeur de manifeste cinématiques avec keyframes caméra, dialogue cues et preview canvas + +## Pas encore implémenté + +- système de missions complet +- système de zones +- branches de dialogues gameplay au-delà des déclencheurs prototype actuels +- flow de chargement +- minimap et HUD de mission +- séparation complète production / debug pour les scènes gameplay +`; + +export const editorFr = `# Éditeur de carte + +L'éditeur de carte est disponible sur "/editor". Il permet d'inspecter et d'ajuster les objets déclarés dans "/public/map.json" directement depuis le navigateur. + +## Ce qui est édité + +L'éditeur travaille sur la liste de nodes stockée dans "/public/map.json". + +Chaque node décrit un objet de la scène : + +- "name" : nom du dossier modèle dans "/public/models/{name}/model.glb", avec fallback vers "model.gltf" +- "type" : catégorie de l'objet +- "position" : "[x, y, z]" +- "rotation" : "[x, y, z]" +- "scale" : "[x, y, z]" + +Les modèles sont chargés depuis "/public/models". Si un modèle manque, l'éditeur affiche un cube gris de remplacement pour que le node reste sélectionnable et déplaçable. + +## Workflow de base + +1. Ouvrir "/editor". +2. Sélectionner un objet dans la vue 3D. +3. Choisir un mode de transformation : translation, rotation ou scale. +4. Déplacer la gizmo de transformation. +5. Utiliser undo ou redo si nécessaire. +6. Exporter le JSON mis à jour ou le sauvegarder sur le serveur de dev. + +## Contrôles + +| Action | Input | +| --- | --- | +| Sélectionner un objet | Clic sur l'objet | +| Désélectionner | "Esc" ou clic dans le vide | +| Mode translation | "T" | +| Mode rotation | "R" | +| Mode scale | "S" | +| Undo | "Ctrl+Z" | +| Redo | "Ctrl+Y" | +| Déplacement en vue verrouillée | "WASD", "ZQSD", flèches | +| Monter / descendre | "Space", "Shift" | + +## Actions fichier + +### Export JSON + +"Export JSON" télécharge la liste actuelle des nodes sous le nom "map.json". À utiliser pour remplacer manuellement "/public/map.json". + +### Save to server + +"Save to server" est disponible uniquement en développement local. L'action écrit la carte modifiée dans "/public/map.json" via l'endpoint du serveur de dev Vite. + +Cette action est masquée dans les builds de production car il n'existe pas encore d'API de persistance production. + +## Éditer les dialogues et sous-titres + +Le panneau latéral contient aussi des outils pour les dialogues et les sous-titres. + +### Manifeste dialogues + +Le panneau \`Dialogues\` permet d'éditer \`public/sounds/dialogue/dialogues.json\` sans ouvrir le JSON à la main. + +- \`Reload\` recharge le manifeste depuis le disque. +- \`Add\` crée un dialogue local pour la voix courante et assigne le prochain index SRT disponible. +- \`Save\` écrit le manifeste via le serveur Vite local. +- \`Preview dialogue\` joue le dialogue sélectionné avec les sous-titres dans l'éditeur. +- \`Create FR SRT cue\` crée la cue française si elle manque. +- \`Delete dialogue\` supprime localement l'entrée sélectionnée. + +Après \`Add\`, il faut cliquer \`Save\` pour conserver le dialogue dans le manifeste. La cue SRT FR est écrite directement, mais le manifeste reste local tant qu'il n'est pas sauvegardé. + +Les nouveaux dialogues utilisent un chemin audio placeholder comme \`/sounds/dialogue/new_dialogue_24.mp3\`. Remplace-le par un vrai MP3 avant validation finale. + +### Éditeur SRT + +1. Choisir une voix : \`narrateur\`, \`fermier\` ou \`electricienne\`. +2. Choisir une langue : \`FR\` ou \`EN\`. +3. Modifier le texte SRT directement dans la textarea. +4. Utiliser la preview audio pour vérifier le dialogue sélectionné. +5. Utiliser \`Set start\`, \`Set end\`, \`-100ms\` et \`+100ms\` pour ajuster le timing de la cue sélectionnée avec l'audio. +6. Utiliser \`Save SRT\` en développement local, ou \`Export SRT\` pour télécharger le fichier manuellement. + +Chaque fichier SRT appartient à une voix, pas à un dialogue. Les indexes de cue doivent correspondre aux valeurs \`subtitleCueIndex\` référencées par le manifeste de dialogues. + +## Valider les assets de dialogue + +Utilise \`Validate\` dans le panneau SRT pour vérifier le manifeste et les assets liés. + +La validation vérifie : + +- \`public/sounds/dialogue/dialogues.json\` +- les fichiers audio de dialogue référencés +- les fichiers SRT français +- les indexes de cue référencés par le manifeste + +Les fichiers SRT anglais manquants sont des warnings parce que le runtime retombe sur les sous-titres français. + +## Éditer les cinématiques + +Le panneau \`Cinematics\` permet d'éditer \`public/cinematics.json\`. + +Chaque cinématique contient : + +- un \`id\` +- un \`timecode\` global optionnel +- au moins deux keyframes caméra +- des dialogue cues optionnelles synchronisées avec la timeline + +Les keyframes caméra définissent un temps relatif, une position caméra et une cible de regard. Les dialogue cues définissent un temps relatif et un \`dialogueId\` issu de \`dialogues.json\`. + +Actions disponibles : + +- \`Reload\` recharge le manifeste. +- \`Add\` crée une cinématique locale avec deux keyframes. +- \`Save\` écrit \`public/cinematics.json\` via le serveur Vite local. +- \`Preview cinematic\` joue l'animation caméra dans le canvas éditeur. +- \`Add keyframe\` et \`Remove\` modifient le chemin caméra. +- \`Add dialogue\` et \`Remove\` modifient les dialogues synchronisés. +- \`Delete cinematic\` supprime localement la cinématique sélectionnée. + +Les dialogue cues sont la manière recommandée de synchroniser un dialogue avec une cinématique. Évite de donner aussi un \`timecode\` global au même dialogue dans \`dialogues.json\`, sinon il peut être lancé deux fois. + +## Inspecteur JSON + +Le panneau latéral affiche le JSON brut de la carte : + +- sans sélection, il affiche toute la liste des nodes +- avec un objet sélectionné, il met en évidence les lignes du node sélectionné + +Utilise-le pour vérifier les valeurs numériques exactes avant export ou sauvegarde. + +## Limites actuelles + +- L'éditeur modifie uniquement les nodes existants. +- Il n'y a pas encore d'interface pour créer ou supprimer des objets. +- La sauvegarde production n'est pas implémentée. +- Les modèles manquants s'affichent comme cubes de fallback au lieu de bloquer tout l'éditeur. +- La sauvegarde SRT est un helper local du serveur Vite, pas une API backend de production. +- Les sauvegardes dialogues et cinématiques sont aussi des helpers locaux du serveur Vite. +`; diff --git a/src/data/zones.ts b/src/data/zones.ts new file mode 100644 index 0000000..cf88747 --- /dev/null +++ b/src/data/zones.ts @@ -0,0 +1,19 @@ +import type { Zone } from "@/types/game"; +import type { Vector3Tuple } from "@/types/three/three"; + +export const ZONES: Zone[] = [ + { + id: "fabrikExit", + position: [-5, 25, -15] as Vector3Tuple, + radius: 10, + height: 20, + 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..d1477ae --- /dev/null +++ b/src/hooks/useActivityCity.ts @@ -0,0 +1,5 @@ +import { useGameStore } from "@/managers/stores/useGameStore"; + +export function useActivityCity(): boolean { + return useGameStore((state) => state.missionFlow.activityCity); +} diff --git a/src/managers/AudioManager.ts b/src/managers/AudioManager.ts index 81c09d5..d63d976 100644 --- a/src/managers/AudioManager.ts +++ b/src/managers/AudioManager.ts @@ -114,6 +114,18 @@ export class AudioManager { return audio; } + playSoundWithCallback( + path: string, + volume: number, + onEnded: () => void, + options: PlaySoundOptions = {}, + ): HTMLAudioElement { + const audio = this.playSound(path, volume, options); + audio.addEventListener("ended", onEnded, { once: true }); + + return audio; + } + playMusic(path: string, volume = 1): void { this._musicVolume = AudioManager._clampVolume(volume); diff --git a/src/managers/stores/useGameStore.ts b/src/managers/stores/useGameStore.ts index 36672c8..15489dd 100644 --- a/src/managers/stores/useGameStore.ts +++ b/src/managers/stores/useGameStore.ts @@ -1,6 +1,9 @@ import { create } from "zustand"; +import type { GameStep } from "@/types/game"; import { isRepairMissionId, + getNextMissionStep, + getPreviousMissionStep, type MissionStep, type RepairMissionId, } from "@/types/gameplay/repairMission"; @@ -9,6 +12,7 @@ export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro"; export type { MissionStep, RepairMissionId }; interface IntroState { + currentStep: GameStep; dialogueAudio: string | null; hasCompleted: boolean; isBikeUnlocked: boolean; @@ -19,9 +23,17 @@ interface MissionState { dialogueAudio: string | null; } +interface MissionFlowState { + activityCity: boolean; + canMove: boolean; + dialogMessage: string | null; + playerName: string; +} + interface GameState { mainState: MainGameState; isCinematicPlaying: boolean; + missionFlow: MissionFlowState; intro: IntroState; bike: MissionState & { isRepaired: boolean; @@ -41,7 +53,12 @@ interface GameState { interface GameActions { setMainState: (mainState: MainGameState) => void; setCinematicPlaying: (isCinematicPlaying: boolean) => void; + hideDialog: () => void; + setActivityCity: (activityCity: boolean) => void; + setCanMove: (canMove: boolean) => void; + setIntroStep: (step: GameStep) => void; setIntroState: (intro: Partial) => void; + setPlayerName: (playerName: string) => void; setBikeState: (bike: Partial) => void; setPyloneState: (pylone: Partial) => void; setFermeState: (ferme: Partial) => void; @@ -56,51 +73,12 @@ interface GameActions { advanceGameState: () => void; rewindGameState: () => void; resetGame: () => void; + showDialog: (dialogMessage: string) => 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": - return "reassembling"; - case "reassembling": - case "done": - return "done"; - } -} - -function getPreviousMissionStep(step: MissionStep): MissionStep { - switch (step) { - case "locked": - case "waiting": - return "locked"; - case "inspected": - return "waiting"; - case "fragmented": - return "inspected"; - case "scanning": - return "fragmented"; - case "repairing": - return "scanning"; - case "reassembling": - return "repairing"; - case "done": - return "reassembling"; - } -} - function completeIntroState(state: GameState): GameStateUpdate { return { mainState: "bike", @@ -225,7 +203,14 @@ function createInitialGameState(): GameState { return { mainState: "intro", isCinematicPlaying: false, + missionFlow: { + activityCity: true, + canMove: false, + dialogMessage: null, + playerName: "", + }, intro: { + currentStep: "intro", dialogueAudio: null, hasCompleted: false, isBikeUnlocked: false, @@ -256,8 +241,26 @@ export const useGameStore = create()((set) => ({ ...createInitialGameState(), setMainState: (mainState) => set({ mainState }), setCinematicPlaying: (isCinematicPlaying) => set({ isCinematicPlaying }), + hideDialog: () => + set((state) => ({ + missionFlow: { ...state.missionFlow, dialogMessage: null }, + })), + setActivityCity: (activityCity) => + set((state) => ({ + missionFlow: { ...state.missionFlow, activityCity }, + })), + setCanMove: (canMove) => + set((state) => ({ + missionFlow: { ...state.missionFlow, canMove }, + })), + setIntroStep: (step: GameStep) => + set((state) => ({ intro: { ...state.intro, currentStep: step } })), setIntroState: (intro) => set((state) => ({ intro: { ...state.intro, ...intro } })), + setPlayerName: (playerName) => + set((state) => ({ + missionFlow: { ...state.missionFlow, playerName }, + })), setBikeState: (bike) => set((state) => ({ bike: { ...state.bike, ...bike } })), setPyloneState: (pylone) => @@ -300,4 +303,8 @@ export const useGameStore = create()((set) => ({ return { outro: { ...state.outro, hasStarted: false } }; }), resetGame: () => set(createInitialGameState()), + showDialog: (dialogMessage) => + set((state) => ({ + missionFlow: { ...state.missionFlow, dialogMessage }, + })), })); diff --git a/src/pages/docs/mission-flow/page.tsx b/src/pages/docs/mission-flow/page.tsx new file mode 100644 index 0000000..bce4e20 --- /dev/null +++ b/src/pages/docs/mission-flow/page.tsx @@ -0,0 +1,14 @@ +import missionFlow from "../../../../docs/technical/mission-flow.md?raw"; +import { DocsDocument } from "@/components/docs/DocsDocument"; +import { missionFlowFr } from "@/data/docs/docsTranslations"; + +export function DocsMissionFlowPage(): React.JSX.Element { + return ( + + ); +} diff --git a/src/pages/page.tsx b/src/pages/page.tsx index d389ef6..a34c6aa 100644 --- a/src/pages/page.tsx +++ b/src/pages/page.tsx @@ -1,9 +1,12 @@ -import { Suspense, useCallback, useState } from "react"; +import { Suspense, useCallback, useEffect, useState } from "react"; import { Canvas } from "@react-three/fiber"; import * as THREE from "three"; import { DebugPerf } from "@/components/debug/DebugPerf"; +import { DialogMessage } from "@/components/ui/DialogMessage"; import { GameUI } from "@/components/ui/GameUI"; +import { BienvenueDisplay, IntroUI } from "@/components/ui/IntroUI"; import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay"; +import { useGameStore } from "@/managers/stores/useGameStore"; import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider"; import { INITIAL_SCENE_LOADING_STATE, @@ -12,9 +15,26 @@ import { import { World } from "@/world/World"; export function HomePage(): React.JSX.Element { + const dialogMessage = useGameStore( + (state) => state.missionFlow.dialogMessage, + ); + const hideDialog = useGameStore((state) => state.hideDialog); const [sceneLoadingState, setSceneLoadingState] = useState( INITIAL_SCENE_LOADING_STATE, ); + + useEffect(() => { + if (!dialogMessage) return undefined; + + const timeoutId = window.setTimeout(() => { + hideDialog(); + }, 3000); + + return () => { + window.clearTimeout(timeoutId); + }; + }, [dialogMessage, hideDialog]); + const handleSceneLoadingStateChange = useCallback( (nextState: SceneLoadingState) => { setSceneLoadingState((currentState) => { @@ -43,6 +63,15 @@ export function HomePage(): React.JSX.Element { + + + {dialogMessage ? ( + + ) : null} ); diff --git a/src/router.tsx b/src/router.tsx index 58ffadd..3790c02 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -17,6 +17,7 @@ import { DocsInteractionRoute, DocsLayoutRoute, DocsMainFeatureRoute, + DocsMissionFlowRoute, DocsReadmeRoute, DocsRepairGameRoute, DocsSceneRuntimeRoute, diff --git a/src/routes/DocsRoute.tsx b/src/routes/DocsRoute.tsx index 83e1203..86f775a 100644 --- a/src/routes/DocsRoute.tsx +++ b/src/routes/DocsRoute.tsx @@ -71,10 +71,6 @@ const LazyDocsZustandPage = lazyNamed( () => import("@/pages/docs/zustand/page"), "DocsZustandPage", ); -const LazyDocsThreeDebuggingPage = lazyNamed( - () => import("@/pages/docs/three-debugging/page"), - "DocsThreeDebuggingPage", -); const LazyDocsFeaturesPage = lazyNamed( () => import("@/pages/docs/features/page"), "DocsFeaturesPage", @@ -95,6 +91,14 @@ const LazyDocsCodeReviewPage = lazyNamed( () => import("@/pages/docs/code-review/page"), "DocsCodeReviewPage", ); +const LazyDocsMissionFlowPage = lazyNamed( + () => import("@/pages/docs/mission-flow/page"), + "DocsMissionFlowPage", +); +const LazyDocsThreeDebuggingPage = lazyNamed( + () => import("@/pages/docs/three-debugging/page"), + "DocsThreeDebuggingPage", +); export const DocsLayoutRoute = createDocsRoute(LazyDocsLayout); export const DocsReadmeRoute = createDocsRoute(LazyDocsReadmePage); @@ -111,11 +115,12 @@ export const DocsTechnicalEditorRoute = createDocsRoute( export const DocsAudioRoute = createDocsRoute(LazyDocsAudioPage); export const DocsHandTrackingRoute = createDocsRoute(LazyDocsHandTrackingPage); export const DocsZustandRoute = createDocsRoute(LazyDocsZustandPage); -export const DocsThreeDebuggingRoute = createDocsRoute( - LazyDocsThreeDebuggingPage, -); export const DocsFeaturesRoute = createDocsRoute(LazyDocsFeaturesPage); export const DocsMainFeatureRoute = createDocsRoute(LazyDocsMainFeaturePage); export const DocsEditorRoute = createDocsRoute(LazyDocsEditorPage); export const DocsAnimationRoute = createDocsRoute(LazyDocsAnimationPage); export const DocsCodeReviewRoute = createDocsRoute(LazyDocsCodeReviewPage); +export const DocsMissionFlowRoute = createDocsRoute(LazyDocsMissionFlowPage); +export const DocsThreeDebuggingRoute = createDocsRoute( + LazyDocsThreeDebuggingPage, +); diff --git a/src/types/game.ts b/src/types/game.ts new file mode 100644 index 0000000..44bd7a5 --- /dev/null +++ b/src/types/game.ts @@ -0,0 +1,34 @@ +import type { Vector3Tuple } from "@/types/three/three"; + +export type GameStep = + | "intro" + | "start-intro" + | "naming" + | "bienvenue" + | "star-move" + | "mission2" + | "searching" + | "helped" + | "manipulation" + | "outOfFabrik"; + +export const GAME_STEPS: readonly GameStep[] = [ + "intro", + "start-intro", + "naming", + "bienvenue", + "star-move", + "mission2", + "searching", + "helped", + "manipulation", + "outOfFabrik", +] as const; + +export interface Zone { + id: string; + position: Vector3Tuple; + radius: number; + height: number; + targetStep: GameStep; +} diff --git a/src/types/gameplay/repairMission.ts b/src/types/gameplay/repairMission.ts index f8836b0..dc78e93 100644 --- a/src/types/gameplay/repairMission.ts +++ b/src/types/gameplay/repairMission.ts @@ -30,3 +30,43 @@ export function isRepairMissionId(value: string): value is RepairMissionId { export function isMissionStep(value: string): value is MissionStep { return (MISSION_STEPS as readonly string[]).includes(value); } + +export 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": + return "reassembling"; + case "reassembling": + case "done": + return "done"; + } +} + +export function getPreviousMissionStep(step: MissionStep): MissionStep { + switch (step) { + case "locked": + case "waiting": + return "locked"; + case "inspected": + return "waiting"; + case "fragmented": + return "inspected"; + case "scanning": + return "fragmented"; + case "repairing": + return "scanning"; + case "reassembling": + return "repairing"; + case "done": + return "reassembling"; + } +} diff --git a/src/world/World.tsx b/src/world/World.tsx index 4198367..bad4fc2 100644 --- a/src/world/World.tsx +++ b/src/world/World.tsx @@ -9,9 +9,16 @@ import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot"; import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading"; import { useGameStore } from "@/managers/stores/useGameStore"; +import { GameFlow } from "@/components/game/GameFlow"; +import { + ZoneDebugVisuals, + ZoneDetection, +} from "@/components/zone/ZoneDetection"; import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls"; import { DebugHelpers } from "@/components/debug/scene/DebugHelpers"; import { HandTrackingGlove } from "@/components/three/handTracking/HandTrackingGlove"; +import { PyloneDestroyed } from "@/components/three/interaction/PyloneDestroyed"; +import { NPCHelper } from "@/components/three/interaction/NPCHelper"; import { Environment } from "@/world/Environment"; import { GameCinematics } from "@/world/GameCinematics"; import { GameDialogues } from "@/world/GameDialogues"; @@ -65,6 +72,11 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element { {cameraMode === "debug" ? : null} {sceneMode === "game" ? ( <> + + + + + state.missionFlow.canMove); + const capsule = useRef(createSpawnCapsule(spawnPosition)); useLayoutEffect(() => { @@ -209,7 +210,7 @@ export function PlayerController({ useFrame((_, delta) => { if (!initializedRef.current) return; - if (isPlayerInputLocked()) { + if (isPlayerInputLocked() || !canMove) { keys.current = { ...DEFAULT_KEYS }; velocity.current.set(0, 0, 0); wantsJump.current = false;