Compare commits
6 Commits
design
...
2c3f0db65b
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c3f0db65b | |||
| 91ebea8d99 | |||
| f7b968abe7 | |||
| 32d644b09d | |||
| 1b7813a5bb | |||
| 41f7b2ad19 |
+187
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -59,6 +59,7 @@ Rule of thumb:
|
|||||||
The store exposes:
|
The store exposes:
|
||||||
|
|
||||||
- `mainState`: the active game phase
|
- `mainState`: the active game phase
|
||||||
|
- `missionFlow`: intro and mission 2 prototype state
|
||||||
- `intro`: intro-specific state
|
- `intro`: intro-specific state
|
||||||
- `bike`: e-bike mission state
|
- `bike`: e-bike mission state
|
||||||
- `pylone`: power grid mission state
|
- `pylone`: power grid mission state
|
||||||
@@ -66,6 +67,8 @@ The store exposes:
|
|||||||
- `outro`: ending state
|
- `outro`: ending state
|
||||||
- actions for direct updates and progression updates
|
- actions for direct updates and progression updates
|
||||||
|
|
||||||
|
The `missionFlow` slice contains the prototype step, player name, movement lock, city activity flag, and temporary dialog message. It is in the main game store because it is global gameplay state used by UI, world components, and the player controller.
|
||||||
|
|
||||||
The mission steps currently use this sequence:
|
The mission steps currently use this sequence:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -141,6 +144,8 @@ For repair missions, it mounts the reusable `RepairGame` component with a missio
|
|||||||
|
|
||||||
Mission-specific behavior stays in `src/data/gameplay/repairMissions.ts`: each mission can define its broken nodes, placeholder targets, scan duration, and reassembly duration without adding mission branches to `RepairGame`.
|
Mission-specific behavior stays in `src/data/gameplay/repairMissions.ts`: each mission can define its broken nodes, placeholder targets, scan duration, and reassembly duration without adding mission branches to `RepairGame`.
|
||||||
|
|
||||||
|
The intro and mission 2 prototype flow is documented separately in `docs/technical/mission-flow.md`. It intentionally uses the same `useGameStore` source of truth instead of a dedicated `GameStepManager` or a second Zustand store.
|
||||||
|
|
||||||
That means the scene can progressively move toward this pattern:
|
That means the scene can progressively move toward this pattern:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@@ -171,8 +176,9 @@ Current overlays:
|
|||||||
- `Crosshair`: player aiming helper
|
- `Crosshair`: player aiming helper
|
||||||
- `InteractPrompt`: interaction prompt
|
- `InteractPrompt`: interaction prompt
|
||||||
- `RepairMovementLockIndicator`: player-facing indicator shown while repair steps temporarily disable movement
|
- `RepairMovementLockIndicator`: player-facing indicator shown while repair steps temporarily disable movement
|
||||||
|
- Mission flow overlays such as `IntroUI`, `BienvenueDisplay`, and `DialogMessage` are mounted by `src/pages/page.tsx` because they are route-level HTML overlays rather than persistent game HUD elements.
|
||||||
|
|
||||||
`src/pages/page.tsx` should stay thin and mount only the canvas and `GameUI`.
|
`src/pages/page.tsx` should stay thin and mount the canvas, persistent `GameUI`, and route-level overlays.
|
||||||
|
|
||||||
## Regression Rules
|
## Regression Rules
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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.missionFlow.step);
|
||||||
|
const setStep = useGameStore((state) => state.setFlowStep);
|
||||||
|
const setActivityCity = useGameStore((state) => state.setActivityCity);
|
||||||
|
const setCanMove = useGameStore((state) => state.setCanMove);
|
||||||
|
const hasInitialized = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("[GameFlow] Current step:", step);
|
||||||
|
if (!hasInitialized.current && step === "intro") {
|
||||||
|
hasInitialized.current = true;
|
||||||
|
console.log("[GameFlow] Transition to start-intro");
|
||||||
|
setStep("start-intro");
|
||||||
|
}
|
||||||
|
}, [step, setStep]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("[GameFlow] useEffect triggered, step:", step);
|
||||||
|
|
||||||
|
if (step === "start-intro") {
|
||||||
|
console.log("[GameFlow] Playing intro audio");
|
||||||
|
const audio = AudioManager.getInstance();
|
||||||
|
audio.playSoundWithCallback(AUDIO_PATHS.intro, 0.5, () => {
|
||||||
|
console.log("[GameFlow] Intro audio ended, transition to naming");
|
||||||
|
setStep("naming");
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === "bienvenue") {
|
||||||
|
console.log("[GameFlow] Playing bienvenue audio");
|
||||||
|
const audio = AudioManager.getInstance();
|
||||||
|
audio.playSoundWithCallback(AUDIO_PATHS.bienvenue, 0.5, () => {
|
||||||
|
console.log("[GameFlow] Bienvenue audio ended, enable movement");
|
||||||
|
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, setStep, setActivityCity, setCanMove]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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 CentralObjectProps {
|
||||||
|
position: Vector3Tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CentralObject({
|
||||||
|
position,
|
||||||
|
}: CentralObjectProps): React.JSX.Element {
|
||||||
|
const step = useGameStore((state) => state.missionFlow.step);
|
||||||
|
const setStep = useGameStore((state) => state.setFlowStep);
|
||||||
|
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 (
|
||||||
|
<InteractableObject
|
||||||
|
kind="trigger"
|
||||||
|
label="central"
|
||||||
|
position={position}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
<group position={position}>
|
||||||
|
<mesh>
|
||||||
|
<boxGeometry args={[1, 1, 1]} />
|
||||||
|
<meshStandardMaterial color="orange" />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
</InteractableObject>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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 VillageoisHelperObjectProps {
|
||||||
|
position: Vector3Tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VillageoisHelperObject({
|
||||||
|
position,
|
||||||
|
}: VillageoisHelperObjectProps): React.JSX.Element {
|
||||||
|
const step = useGameStore((state) => state.missionFlow.step);
|
||||||
|
const setStep = useGameStore((state) => state.setFlowStep);
|
||||||
|
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 (
|
||||||
|
<InteractableObject
|
||||||
|
kind="trigger"
|
||||||
|
label="villageois_helper"
|
||||||
|
position={position}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
<group position={position}>
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry args={[0.5, 16, 16]} />
|
||||||
|
<meshStandardMaterial color="cyan" />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
</InteractableObject>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "20%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.9)",
|
||||||
|
padding: "1rem 2rem",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "2px solid #fff",
|
||||||
|
zIndex: 200,
|
||||||
|
maxWidth: "80%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
color: "#fff",
|
||||||
|
margin: 0,
|
||||||
|
fontSize: "1rem",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
|
|
||||||
|
export function IntroUI(): React.JSX.Element | null {
|
||||||
|
const step = useGameStore((state) => state.missionFlow.step);
|
||||||
|
const setPlayerName = useGameStore((state) => state.setPlayerName);
|
||||||
|
const setStep = useGameStore((state) => state.setFlowStep);
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
|
||||||
|
if (step !== "naming") return null;
|
||||||
|
|
||||||
|
const handleSubmit = (): void => {
|
||||||
|
if (inputValue.trim() === "") return;
|
||||||
|
|
||||||
|
console.log("[IntroUI] Submitting, name:", inputValue.trim());
|
||||||
|
setPlayerName(inputValue.trim());
|
||||||
|
console.log("[IntroUI] Calling transitionTo('bienvenue')");
|
||||||
|
setStep("bienvenue");
|
||||||
|
console.log("[IntroUI] After transitionTo, step should be:", step);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent): void => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
padding: "2rem",
|
||||||
|
borderRadius: "12px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1.5rem",
|
||||||
|
minWidth: "300px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
color: "#fff",
|
||||||
|
margin: 0,
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Quel est votre prénom ?
|
||||||
|
</h2>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Votre prénom"
|
||||||
|
autoFocus
|
||||||
|
style={{
|
||||||
|
padding: "0.75rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
borderRadius: "6px",
|
||||||
|
border: "1px solid #444",
|
||||||
|
backgroundColor: "#2a2a2a",
|
||||||
|
color: "#fff",
|
||||||
|
outline: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={inputValue.trim() === ""}
|
||||||
|
style={{
|
||||||
|
padding: "0.75rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
borderRadius: "6px",
|
||||||
|
border: "none",
|
||||||
|
backgroundColor: inputValue.trim() ? "#4a9" : "#444",
|
||||||
|
color: "#fff",
|
||||||
|
cursor: inputValue.trim() ? "pointer" : "not-allowed",
|
||||||
|
transition: "background-color 0.2s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BienvenueDisplay(): React.JSX.Element | null {
|
||||||
|
const step = useGameStore((state) => state.missionFlow.step);
|
||||||
|
const playerName = useGameStore((state) => state.missionFlow.playerName);
|
||||||
|
|
||||||
|
if (step !== "bienvenue") return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: "20%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||||
|
padding: "1rem 2rem",
|
||||||
|
borderRadius: "8px",
|
||||||
|
zIndex: 100,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
color: "#fff",
|
||||||
|
margin: 0,
|
||||||
|
fontSize: "1.25rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bienvenue {playerName} !
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
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 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",
|
||||||
|
"helped",
|
||||||
|
"manipulation",
|
||||||
|
"outOfFabrik",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ZoneDetection(): null {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
const triggeredZones = useRef<Set<string>>(new Set());
|
||||||
|
const debug = Debug.getInstance();
|
||||||
|
const step = useGameStore((state) => state.missionFlow.step);
|
||||||
|
const setStep = useGameStore((state) => state.setFlowStep);
|
||||||
|
|
||||||
|
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.missionFlow.step;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ZoneVisualProps {
|
||||||
|
position: [number, number, number];
|
||||||
|
radius: number;
|
||||||
|
height: number;
|
||||||
|
triggered: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ZoneVisual({
|
||||||
|
position,
|
||||||
|
radius,
|
||||||
|
height,
|
||||||
|
triggered,
|
||||||
|
}: ZoneVisualProps): React.JSX.Element {
|
||||||
|
const color = triggered ? "#00ff00" : "#ff0000";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group position={position}>
|
||||||
|
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||||
|
<ringGeometry args={[radius - 0.3, radius, 32]} />
|
||||||
|
<meshBasicMaterial color={color} side={THREE.DoubleSide} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, height / 2, 0]}>
|
||||||
|
<cylinderGeometry args={[radius, radius, height, 32, 1, true]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
color={color}
|
||||||
|
transparent
|
||||||
|
opacity={0.15}
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ZoneDebugVisuals(): React.JSX.Element | null {
|
||||||
|
const debug = Debug.getInstance();
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
const [triggeredZones, setTriggeredZones] = useState<Set<string>>(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) => (
|
||||||
|
<ZoneVisual
|
||||||
|
key={zone.id}
|
||||||
|
position={[zone.position[0], 0.1, zone.position[2]]}
|
||||||
|
radius={zone.radius}
|
||||||
|
height={zone.height}
|
||||||
|
triggered={triggeredZones.has(zone.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +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;
|
||||||
@@ -50,6 +50,12 @@ export const docGroups: DocGroup[] = [
|
|||||||
subtitle: "Progression store",
|
subtitle: "Progression store",
|
||||||
meta: "06",
|
meta: "06",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/docs/mission-flow",
|
||||||
|
title: "Mission Flow",
|
||||||
|
subtitle: "Intro and mission 2 prototype",
|
||||||
|
meta: "07",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -59,25 +65,25 @@ export const docGroups: DocGroup[] = [
|
|||||||
path: "/docs/features",
|
path: "/docs/features",
|
||||||
title: "Features",
|
title: "Features",
|
||||||
subtitle: "Implemented scope",
|
subtitle: "Implemented scope",
|
||||||
meta: "07",
|
meta: "08",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/main-feature",
|
path: "/docs/main-feature",
|
||||||
title: "Main Feature",
|
title: "Main Feature",
|
||||||
subtitle: "Repair-game prototype",
|
subtitle: "Repair-game prototype",
|
||||||
meta: "08",
|
meta: "09",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/editor",
|
path: "/docs/editor",
|
||||||
title: "Editor User Guide",
|
title: "Editor User Guide",
|
||||||
subtitle: "Editing workflow",
|
subtitle: "Editing workflow",
|
||||||
meta: "09",
|
meta: "10",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/animation",
|
path: "/docs/animation",
|
||||||
title: "Animation & 3D Model System",
|
title: "Animation & 3D Model System",
|
||||||
subtitle: "Components and usage",
|
subtitle: "Components and usage",
|
||||||
meta: "010",
|
meta: "11",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -328,6 +328,7 @@ Règle simple :
|
|||||||
Le store expose :
|
Le store expose :
|
||||||
|
|
||||||
- \`mainState\` : phase active du jeu
|
- \`mainState\` : phase active du jeu
|
||||||
|
- \`missionFlow\` : état prototype de l'intro et de la mission 2
|
||||||
- \`intro\` : état spécifique à l'intro
|
- \`intro\` : état spécifique à l'intro
|
||||||
- \`bike\` : état de la mission vélo
|
- \`bike\` : état de la mission vélo
|
||||||
- \`pylone\` : état de la mission réseau électrique
|
- \`pylone\` : état de la mission réseau électrique
|
||||||
@@ -335,6 +336,8 @@ Le store expose :
|
|||||||
- \`outro\` : état de fin
|
- \`outro\` : état de fin
|
||||||
- des actions de mise à jour directe et des actions de progression
|
- 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 :
|
Les étapes de mission utilisent actuellement cette séquence :
|
||||||
|
|
||||||
\`\`\`ts
|
\`\`\`ts
|
||||||
@@ -401,6 +404,8 @@ Pour les missions de réparation, il monte le composant réutilisable \`RepairGa
|
|||||||
|
|
||||||
\`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\`.
|
\`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 :
|
La scène peut donc évoluer progressivement vers ce pattern :
|
||||||
|
|
||||||
\`\`\`tsx
|
\`\`\`tsx
|
||||||
@@ -431,8 +436,9 @@ Overlays actuels :
|
|||||||
- \`Crosshair\` : aide de visée joueur
|
- \`Crosshair\` : aide de visée joueur
|
||||||
- \`InteractPrompt\` : prompt d'interaction
|
- \`InteractPrompt\` : prompt d'interaction
|
||||||
- \`RepairMovementLockIndicator\` : indicateur joueur affiché quand les étapes repair désactivent temporairement le déplacement
|
- \`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 seulement le canvas et \`GameUI\`.
|
\`src/pages/page.tsx\` doit rester fin et monter le canvas, le \`GameUI\` persistant et les overlays de route.
|
||||||
|
|
||||||
## Règles anti-régression
|
## Règles anti-régression
|
||||||
|
|
||||||
@@ -448,6 +454,78 @@ Overlays actuels :
|
|||||||
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.
|
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
|
export const featuresFr = `# Fonctionnalités implémentées
|
||||||
|
|
||||||
Ce document liste les fonctionnalités présentes dans le code actuel.
|
Ce document liste les fonctionnalités présentes dans le code actuel.
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
|
|
||||||
|
export function useActivityCity(): boolean {
|
||||||
|
return useGameStore((state) => state.missionFlow.activityCity);
|
||||||
|
}
|
||||||
@@ -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<DialogState>({
|
||||||
|
message: "",
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const showDialog = (message: string): void => {
|
||||||
|
setDialog({ message, visible: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideDialog = (): void => {
|
||||||
|
setDialog((prev) => ({ ...prev, visible: false }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return { dialog, showDialog, hideDialog };
|
||||||
|
}
|
||||||
@@ -114,6 +114,18 @@ export class AudioManager {
|
|||||||
return audio;
|
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 {
|
playMusic(path: string, volume = 1): void {
|
||||||
this._musicVolume = AudioManager._clampVolume(volume);
|
this._musicVolume = AudioManager._clampVolume(volume);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import type { GameStep } from "@/types/game";
|
||||||
import {
|
import {
|
||||||
isRepairMissionId,
|
isRepairMissionId,
|
||||||
type MissionStep,
|
type MissionStep,
|
||||||
@@ -19,9 +20,18 @@ interface MissionState {
|
|||||||
dialogueAudio: string | null;
|
dialogueAudio: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MissionFlowState {
|
||||||
|
activityCity: boolean;
|
||||||
|
canMove: boolean;
|
||||||
|
dialogMessage: string | null;
|
||||||
|
playerName: string;
|
||||||
|
step: GameStep;
|
||||||
|
}
|
||||||
|
|
||||||
interface GameState {
|
interface GameState {
|
||||||
mainState: MainGameState;
|
mainState: MainGameState;
|
||||||
isCinematicPlaying: boolean;
|
isCinematicPlaying: boolean;
|
||||||
|
missionFlow: MissionFlowState;
|
||||||
intro: IntroState;
|
intro: IntroState;
|
||||||
bike: MissionState & {
|
bike: MissionState & {
|
||||||
isRepaired: boolean;
|
isRepaired: boolean;
|
||||||
@@ -41,7 +51,12 @@ interface GameState {
|
|||||||
interface GameActions {
|
interface GameActions {
|
||||||
setMainState: (mainState: MainGameState) => void;
|
setMainState: (mainState: MainGameState) => void;
|
||||||
setCinematicPlaying: (isCinematicPlaying: boolean) => void;
|
setCinematicPlaying: (isCinematicPlaying: boolean) => void;
|
||||||
|
hideDialog: () => void;
|
||||||
|
setActivityCity: (activityCity: boolean) => void;
|
||||||
|
setCanMove: (canMove: boolean) => void;
|
||||||
|
setFlowStep: (step: GameStep) => void;
|
||||||
setIntroState: (intro: Partial<IntroState>) => void;
|
setIntroState: (intro: Partial<IntroState>) => void;
|
||||||
|
setPlayerName: (playerName: string) => void;
|
||||||
setBikeState: (bike: Partial<GameState["bike"]>) => void;
|
setBikeState: (bike: Partial<GameState["bike"]>) => void;
|
||||||
setPyloneState: (pylone: Partial<GameState["pylone"]>) => void;
|
setPyloneState: (pylone: Partial<GameState["pylone"]>) => void;
|
||||||
setFermeState: (ferme: Partial<GameState["ferme"]>) => void;
|
setFermeState: (ferme: Partial<GameState["ferme"]>) => void;
|
||||||
@@ -56,6 +71,7 @@ interface GameActions {
|
|||||||
advanceGameState: () => void;
|
advanceGameState: () => void;
|
||||||
rewindGameState: () => void;
|
rewindGameState: () => void;
|
||||||
resetGame: () => void;
|
resetGame: () => void;
|
||||||
|
showDialog: (dialogMessage: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type GameStore = GameState & GameActions;
|
type GameStore = GameState & GameActions;
|
||||||
@@ -225,6 +241,13 @@ function createInitialGameState(): GameState {
|
|||||||
return {
|
return {
|
||||||
mainState: "intro",
|
mainState: "intro",
|
||||||
isCinematicPlaying: false,
|
isCinematicPlaying: false,
|
||||||
|
missionFlow: {
|
||||||
|
activityCity: true,
|
||||||
|
canMove: false,
|
||||||
|
dialogMessage: null,
|
||||||
|
playerName: "",
|
||||||
|
step: "intro",
|
||||||
|
},
|
||||||
intro: {
|
intro: {
|
||||||
dialogueAudio: null,
|
dialogueAudio: null,
|
||||||
hasCompleted: false,
|
hasCompleted: false,
|
||||||
@@ -256,8 +279,26 @@ export const useGameStore = create<GameStore>()((set) => ({
|
|||||||
...createInitialGameState(),
|
...createInitialGameState(),
|
||||||
setMainState: (mainState) => set({ mainState }),
|
setMainState: (mainState) => set({ mainState }),
|
||||||
setCinematicPlaying: (isCinematicPlaying) => set({ isCinematicPlaying }),
|
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 },
|
||||||
|
})),
|
||||||
|
setFlowStep: (step) =>
|
||||||
|
set((state) => ({ missionFlow: { ...state.missionFlow, step } })),
|
||||||
setIntroState: (intro) =>
|
setIntroState: (intro) =>
|
||||||
set((state) => ({ intro: { ...state.intro, ...intro } })),
|
set((state) => ({ intro: { ...state.intro, ...intro } })),
|
||||||
|
setPlayerName: (playerName) =>
|
||||||
|
set((state) => ({
|
||||||
|
missionFlow: { ...state.missionFlow, playerName },
|
||||||
|
})),
|
||||||
setBikeState: (bike) =>
|
setBikeState: (bike) =>
|
||||||
set((state) => ({ bike: { ...state.bike, ...bike } })),
|
set((state) => ({ bike: { ...state.bike, ...bike } })),
|
||||||
setPyloneState: (pylone) =>
|
setPyloneState: (pylone) =>
|
||||||
@@ -300,4 +341,8 @@ export const useGameStore = create<GameStore>()((set) => ({
|
|||||||
return { outro: { ...state.outro, hasStarted: false } };
|
return { outro: { ...state.outro, hasStarted: false } };
|
||||||
}),
|
}),
|
||||||
resetGame: () => set(createInitialGameState()),
|
resetGame: () => set(createInitialGameState()),
|
||||||
|
showDialog: (dialogMessage) =>
|
||||||
|
set((state) => ({
|
||||||
|
missionFlow: { ...state.missionFlow, dialogMessage },
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<DocsDocument
|
||||||
|
content={missionFlow}
|
||||||
|
frContent={missionFlowFr}
|
||||||
|
meta="07"
|
||||||
|
title="Mission Flow"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
+30
-1
@@ -1,9 +1,12 @@
|
|||||||
import { Suspense, useCallback, useState } from "react";
|
import { Suspense, useCallback, useEffect, useState } from "react";
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { DebugPerf } from "@/components/debug/DebugPerf";
|
import { DebugPerf } from "@/components/debug/DebugPerf";
|
||||||
|
import { DialogMessage } from "@/components/ui/DialogMessage";
|
||||||
import { GameUI } from "@/components/ui/GameUI";
|
import { GameUI } from "@/components/ui/GameUI";
|
||||||
|
import { BienvenueDisplay, IntroUI } from "@/components/ui/IntroUI";
|
||||||
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
|
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
|
||||||
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
|
import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
|
||||||
import {
|
import {
|
||||||
INITIAL_SCENE_LOADING_STATE,
|
INITIAL_SCENE_LOADING_STATE,
|
||||||
@@ -12,9 +15,26 @@ import {
|
|||||||
import { World } from "@/world/World";
|
import { World } from "@/world/World";
|
||||||
|
|
||||||
export function HomePage(): React.JSX.Element {
|
export function HomePage(): React.JSX.Element {
|
||||||
|
const dialogMessage = useGameStore(
|
||||||
|
(state) => state.missionFlow.dialogMessage,
|
||||||
|
);
|
||||||
|
const hideDialog = useGameStore((state) => state.hideDialog);
|
||||||
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
||||||
INITIAL_SCENE_LOADING_STATE,
|
INITIAL_SCENE_LOADING_STATE,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialogMessage) return undefined;
|
||||||
|
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
hideDialog();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [dialogMessage, hideDialog]);
|
||||||
|
|
||||||
const handleSceneLoadingStateChange = useCallback(
|
const handleSceneLoadingStateChange = useCallback(
|
||||||
(nextState: SceneLoadingState) => {
|
(nextState: SceneLoadingState) => {
|
||||||
setSceneLoadingState((currentState) => {
|
setSceneLoadingState((currentState) => {
|
||||||
@@ -43,6 +63,15 @@ export function HomePage(): React.JSX.Element {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
<GameUI />
|
<GameUI />
|
||||||
|
<IntroUI />
|
||||||
|
<BienvenueDisplay />
|
||||||
|
{dialogMessage ? (
|
||||||
|
<DialogMessage
|
||||||
|
message={dialogMessage}
|
||||||
|
duration={3000}
|
||||||
|
onClose={hideDialog}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<SceneLoadingOverlay state={sceneLoadingState} />
|
<SceneLoadingOverlay state={sceneLoadingState} />
|
||||||
</HandTrackingProvider>
|
</HandTrackingProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
DocsHandTrackingRoute,
|
DocsHandTrackingRoute,
|
||||||
DocsLayoutRoute,
|
DocsLayoutRoute,
|
||||||
DocsMainFeatureRoute,
|
DocsMainFeatureRoute,
|
||||||
|
DocsMissionFlowRoute,
|
||||||
DocsReadmeRoute,
|
DocsReadmeRoute,
|
||||||
DocsTargetArchitectureRoute,
|
DocsTargetArchitectureRoute,
|
||||||
DocsTechnicalEditorRoute,
|
DocsTechnicalEditorRoute,
|
||||||
@@ -49,6 +50,7 @@ const docsChildRoutes = [
|
|||||||
{ path: "technical-editor", component: DocsTechnicalEditorRoute },
|
{ path: "technical-editor", component: DocsTechnicalEditorRoute },
|
||||||
{ path: "hand-tracking", component: DocsHandTrackingRoute },
|
{ path: "hand-tracking", component: DocsHandTrackingRoute },
|
||||||
{ path: "zustand", component: DocsZustandRoute },
|
{ path: "zustand", component: DocsZustandRoute },
|
||||||
|
{ path: "mission-flow", component: DocsMissionFlowRoute },
|
||||||
{ path: "features", component: DocsFeaturesRoute },
|
{ path: "features", component: DocsFeaturesRoute },
|
||||||
{ path: "main-feature", component: DocsMainFeatureRoute },
|
{ path: "main-feature", component: DocsMainFeatureRoute },
|
||||||
{ path: "editor", component: DocsEditorRoute },
|
{ path: "editor", component: DocsEditorRoute },
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ const LazyDocsZustandPage = lazyNamed(
|
|||||||
() => import("@/pages/docs/zustand/page"),
|
() => import("@/pages/docs/zustand/page"),
|
||||||
"DocsZustandPage",
|
"DocsZustandPage",
|
||||||
);
|
);
|
||||||
|
const LazyDocsMissionFlowPage = lazyNamed(
|
||||||
|
() => import("@/pages/docs/mission-flow/page"),
|
||||||
|
"DocsMissionFlowPage",
|
||||||
|
);
|
||||||
const LazyDocsFeaturesPage = lazyNamed(
|
const LazyDocsFeaturesPage = lazyNamed(
|
||||||
() => import("@/pages/docs/features/page"),
|
() => import("@/pages/docs/features/page"),
|
||||||
"DocsFeaturesPage",
|
"DocsFeaturesPage",
|
||||||
@@ -83,6 +87,7 @@ export const DocsTechnicalEditorRoute = createDocsRoute(
|
|||||||
);
|
);
|
||||||
export const DocsHandTrackingRoute = createDocsRoute(LazyDocsHandTrackingPage);
|
export const DocsHandTrackingRoute = createDocsRoute(LazyDocsHandTrackingPage);
|
||||||
export const DocsZustandRoute = createDocsRoute(LazyDocsZustandPage);
|
export const DocsZustandRoute = createDocsRoute(LazyDocsZustandPage);
|
||||||
|
export const DocsMissionFlowRoute = createDocsRoute(LazyDocsMissionFlowPage);
|
||||||
export const DocsFeaturesRoute = createDocsRoute(LazyDocsFeaturesPage);
|
export const DocsFeaturesRoute = createDocsRoute(LazyDocsFeaturesPage);
|
||||||
export const DocsMainFeatureRoute = createDocsRoute(LazyDocsMainFeaturePage);
|
export const DocsMainFeatureRoute = createDocsRoute(LazyDocsMainFeaturePage);
|
||||||
export const DocsEditorRoute = createDocsRoute(LazyDocsEditorPage);
|
export const DocsEditorRoute = createDocsRoute(LazyDocsEditorPage);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
export type GameStep =
|
||||||
|
| "intro"
|
||||||
|
| "start-intro"
|
||||||
|
| "naming"
|
||||||
|
| "bienvenue"
|
||||||
|
| "star-move"
|
||||||
|
| "mission2"
|
||||||
|
| "searching"
|
||||||
|
| "helped"
|
||||||
|
| "manipulation"
|
||||||
|
| "outOfFabrik";
|
||||||
|
|
||||||
|
export interface Zone {
|
||||||
|
id: string;
|
||||||
|
position: Vector3Tuple;
|
||||||
|
radius: number;
|
||||||
|
height: number;
|
||||||
|
targetStep: GameStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameState {
|
||||||
|
step: GameStep;
|
||||||
|
}
|
||||||
@@ -8,9 +8,16 @@ import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
|||||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
|
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
|
||||||
import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading";
|
import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading";
|
||||||
|
import { GameFlow } from "@/components/game/GameFlow";
|
||||||
|
import {
|
||||||
|
ZoneDebugVisuals,
|
||||||
|
ZoneDetection,
|
||||||
|
} from "@/components/zone/ZoneDetection";
|
||||||
import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls";
|
import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls";
|
||||||
import { DebugHelpers } from "@/components/debug/scene/DebugHelpers";
|
import { DebugHelpers } from "@/components/debug/scene/DebugHelpers";
|
||||||
import { HandTrackingGlove } from "@/components/three/handTracking/HandTrackingGlove";
|
import { HandTrackingGlove } from "@/components/three/handTracking/HandTrackingGlove";
|
||||||
|
import { CentralObject } from "@/components/three/interaction/CentralObject";
|
||||||
|
import { VillageoisHelperObject } from "@/components/three/interaction/VillageoisHelperObject";
|
||||||
import { Environment } from "@/world/Environment";
|
import { Environment } from "@/world/Environment";
|
||||||
import { GameCinematics } from "@/world/GameCinematics";
|
import { GameCinematics } from "@/world/GameCinematics";
|
||||||
import { GameDialogues } from "@/world/GameDialogues";
|
import { GameDialogues } from "@/world/GameDialogues";
|
||||||
@@ -65,6 +72,11 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
|
|||||||
{cameraMode === "debug" ? <DebugCameraControls /> : null}
|
{cameraMode === "debug" ? <DebugCameraControls /> : null}
|
||||||
{sceneMode === "game" ? (
|
{sceneMode === "game" ? (
|
||||||
<>
|
<>
|
||||||
|
<GameFlow />
|
||||||
|
<ZoneDetection />
|
||||||
|
<ZoneDebugVisuals />
|
||||||
|
<VillageoisHelperObject position={[1, 12, -55]} />
|
||||||
|
<CentralObject position={[1, 15, -45]} />
|
||||||
{noMusic ? null : <GameMusic />}
|
{noMusic ? null : <GameMusic />}
|
||||||
{noCinematics ? null : <GameCinematics />}
|
{noCinematics ? null : <GameCinematics />}
|
||||||
{noDialogues ? null : <GameDialogues />}
|
{noDialogues ? null : <GameDialogues />}
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export function PlayerController({
|
|||||||
const velocity = useRef(new THREE.Vector3());
|
const velocity = useRef(new THREE.Vector3());
|
||||||
const onFloor = useRef(false);
|
const onFloor = useRef(false);
|
||||||
const wantsJump = useRef(false);
|
const wantsJump = useRef(false);
|
||||||
|
const canMove = useGameStore((state) => state.missionFlow.canMove);
|
||||||
|
|
||||||
const capsule = useRef(
|
const capsule = useRef(
|
||||||
new Capsule(
|
new Capsule(
|
||||||
@@ -200,7 +201,7 @@ export function PlayerController({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
if (isPlayerInputLocked()) {
|
if (isPlayerInputLocked() || !canMove) {
|
||||||
keys.current = { ...DEFAULT_KEYS };
|
keys.current = { ...DEFAULT_KEYS };
|
||||||
velocity.current.set(0, 0, 0);
|
velocity.current.set(0, 0, 0);
|
||||||
wantsJump.current = false;
|
wantsJump.current = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user