refactor: clean map gameplay architecture
This commit is contained in:
@@ -23,3 +23,6 @@
|
|||||||
# Video (cinematics)
|
# Video (cinematics)
|
||||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||||
*.webm filter=lfs diff=lfs merge=lfs -text
|
*.webm filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# ML models
|
||||||
|
*.task filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ jobs:
|
|||||||
- name: 📥 Install
|
- name: 📥 Install
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
- name: 🧹 Lint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: 🎨 Format check
|
||||||
|
run: npm run format:check
|
||||||
|
|
||||||
- name: 📦 Build
|
- name: 📦 Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
|||||||
-164
@@ -1,164 +0,0 @@
|
|||||||
# 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 systèmes lisent `activityCity` depuis `useGameStore` pour adapter leur comportement
|
|
||||||
- **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/managers/stores/useGameStore.ts` | Store Zustand pour l'état global du jeu |
|
|
||||||
| `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/world/GameStageContent.tsx` | Monte les contenus de mission dans la scène |
|
|
||||||
| `src/data/audioConfig.ts` | Chemins des fichiers audio |
|
|
||||||
| `src/data/zones.ts` | Configuration des zones de transition |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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/managers/stores/useGameStore.ts
|
|
||||||
interface GameState {
|
|
||||||
mainState: MainGameState;
|
|
||||||
missionFlow: {
|
|
||||||
activityCity: boolean;
|
|
||||||
canMove: boolean;
|
|
||||||
playerName: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
|
||||||
@@ -10,7 +10,7 @@ The current prototype puts the player in a repair-oriented world where they prog
|
|||||||
- Production map loaded from `public/map.json`
|
- Production map loaded from `public/map.json`
|
||||||
- Progressive map/model/collision/stage loading overlay
|
- Progressive map/model/collision/stage loading overlay
|
||||||
- Player controller with pointer lock, `ZQSD` movement, jump, octree collision, trigger input, and grab input
|
- Player controller with pointer lock, `ZQSD` movement, jump, octree collision, trigger input, and grab input
|
||||||
- Reusable repair-game flow for `bike`, `pylone`, and `ferme`
|
- Reusable repair-game flow for `ebike`, `pylon`, and `farm`
|
||||||
- Repair case animation, exploded model scan, broken-part markers, grabbable replacements, snap-to-placeholder placement, install validation, reassembly, and completion
|
- Repair case animation, exploded model scan, broken-part markers, grabbable replacements, snap-to-placeholder placement, install validation, reassembly, and completion
|
||||||
- Shared interaction system for trigger and grab objects
|
- Shared interaction system for trigger and grab objects
|
||||||
- Rapier physics for gameplay objects while the player keeps a Three.js octree collision controller
|
- Rapier physics for gameplay objects while the player keeps a Three.js octree collision controller
|
||||||
@@ -112,7 +112,7 @@ npm run build
|
|||||||
|
|
||||||
## Optional Hand-Tracking Backend
|
## Optional Hand-Tracking Backend
|
||||||
|
|
||||||
The app can use browser-side MediaPipe, but the default debug source is the local backend.
|
The app can use the local Python backend, but the default debug source is browser-side MediaPipe.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3.11 -m venv backend/.venv
|
python3.11 -m venv backend/.venv
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ La-Fabrik est une expérience web 3D en React, Vite, Three.js et React Three Fib
|
|||||||
Le joueur est dans un monde 3D et avance dans une progression de réparation :
|
Le joueur est dans un monde 3D et avance dans une progression de réparation :
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
intro -> bike -> pylone -> ferme -> outro
|
intro -> ebike -> pylon -> farm -> outro
|
||||||
```
|
```
|
||||||
|
|
||||||
Les trois piliers à connaître pour la review :
|
Les trois piliers à connaître pour la review :
|
||||||
@@ -62,7 +62,7 @@ HomePage
|
|||||||
-> World
|
-> World
|
||||||
-> GameMap
|
-> GameMap
|
||||||
-> GameStageContent
|
-> GameStageContent
|
||||||
-> RepairGame bike/pylone/ferme
|
-> RepairGame ebike/pylon/farm
|
||||||
-> GameMusic
|
-> GameMusic
|
||||||
-> GameDialogues
|
-> GameDialogues
|
||||||
-> Player
|
-> Player
|
||||||
@@ -324,7 +324,7 @@ Ouvrir dans cet ordre :
|
|||||||
`RepairGame` reçoit une mission :
|
`RepairGame` reçoit une mission :
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<RepairGame mission="bike" position={[8, 0, -6]} />
|
<RepairGame mission="ebike" position={[42.2399, 4.5484, 34.6468]} />
|
||||||
```
|
```
|
||||||
|
|
||||||
Puis il vérifie :
|
Puis il vérifie :
|
||||||
@@ -347,7 +347,7 @@ Les variations mission sont dans `repairMissions.ts` :
|
|||||||
### Pourquoi c'est bien
|
### Pourquoi c'est bien
|
||||||
|
|
||||||
- Un seul flow réutilisable.
|
- Un seul flow réutilisable.
|
||||||
- Moins de duplication entre `bike`, `pylone`, `ferme`.
|
- Moins de duplication entre `ebike`, `pylon`, `farm`.
|
||||||
- Les règles générales restent dans les composants.
|
- Les règles générales restent dans les composants.
|
||||||
- Les variations restent dans la data.
|
- Les variations restent dans la data.
|
||||||
- Le debug panel peut tester les mêmes steps que le vrai jeu.
|
- Le debug panel peut tester les mêmes steps que le vrai jeu.
|
||||||
@@ -471,9 +471,9 @@ Main states :
|
|||||||
|
|
||||||
```txt
|
```txt
|
||||||
intro
|
intro
|
||||||
bike
|
ebike
|
||||||
pylone
|
pylon
|
||||||
ferme
|
farm
|
||||||
outro
|
outro
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ It owns:
|
|||||||
|
|
||||||
- `mainState`
|
- `mainState`
|
||||||
- intro state
|
- intro state
|
||||||
- `bike`, `pylone`, and `ferme` mission state
|
- `ebike`, `pylon`, and `farm` mission state
|
||||||
- outro state
|
- outro state
|
||||||
- `isCinematicPlaying`
|
- `isCinematicPlaying`
|
||||||
- progression actions
|
- progression actions
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
# 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 systèmes lisent `activityCity` depuis `useGameStore` pour adapter leur comportement
|
|
||||||
- **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/managers/stores/useGameStore.ts` | Store Zustand pour l'état global du jeu |
|
|
||||||
| `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/world/GameStageContent.tsx` | Monte les contenus de mission dans la scène |
|
|
||||||
| `src/data/audioConfig.ts` | Chemins des fichiers audio |
|
|
||||||
| `src/data/zones.ts` | Configuration des zones de transition |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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/managers/stores/useGameStore.ts
|
|
||||||
interface GameState {
|
|
||||||
mainState: MainGameState;
|
|
||||||
missionFlow: {
|
|
||||||
activityCity: boolean;
|
|
||||||
canMove: boolean;
|
|
||||||
playerName: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
|
||||||
@@ -32,7 +32,7 @@ This keeps hand tracking active while the player is inside an interaction zone,
|
|||||||
|
|
||||||
The production repair activation conditions are:
|
The production repair activation conditions are:
|
||||||
|
|
||||||
- active `mainState` is `bike`, `pylone`, or `ferme`
|
- active `mainState` is `ebike`, `pylon`, or `farm`
|
||||||
- the active mission step is `inspected`, `repairing`, `reassembling`, or `done`
|
- the active mission step is `inspected`, `repairing`, `reassembling`, or `done`
|
||||||
|
|
||||||
This keeps the webcam off during `waiting`, `fragmented`, and `scanning`, then enables hand input only when the repair flow is expected to use hands.
|
This keeps the webcam off during `waiting`, `fragmented`, and `scanning`, then enables hand input only when the repair flow is expected to use hands.
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ The repair game is the current core gameplay loop. It gives three missions the s
|
|||||||
|
|
||||||
Implemented missions:
|
Implemented missions:
|
||||||
|
|
||||||
| Mission | Object | Role |
|
| Mission | Object | Role |
|
||||||
| -------- | ------------- | --------------------------------------------- |
|
| ------- | ------------- | --------------------------------------------- |
|
||||||
| `bike` | E-bike | Repair a damaged cooling core |
|
| `ebike` | E-bike | Repair a damaged cooling core |
|
||||||
| `pylone` | Power pylon | Restore relay/panel-like broken parts |
|
| `pylon` | Power pylon | Restore relay/panel-like broken parts |
|
||||||
| `ferme` | Vertical farm | Stabilize irrigation/sensor-like broken parts |
|
| `farm` | Vertical farm | Stabilize irrigation/sensor-like broken parts |
|
||||||
|
|
||||||
## Main Files
|
## Main Files
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ src/managers/stores/useGameStore.ts
|
|||||||
- `setMissionStep(mission, nextStep)`
|
- `setMissionStep(mission, nextStep)`
|
||||||
- `completeMission(mission)`
|
- `completeMission(mission)`
|
||||||
|
|
||||||
The important architectural choice is that reusable repair components do not call `setBikeState`, `setPyloneState`, or `setFermeState` directly. They use generic mission actions so the same component can run for all three missions.
|
The important architectural choice is that reusable repair components do not call `setEbikeState`, `setPylonState`, or `setFarmState` directly. They use generic mission actions so the same component can run for all three missions.
|
||||||
|
|
||||||
## Data-Driven Mission Config
|
## Data-Driven Mission Config
|
||||||
|
|
||||||
@@ -324,9 +324,9 @@ src/world/GameStageContent.tsx
|
|||||||
Current positions:
|
Current positions:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<RepairGame mission="bike" position={[8, 0, -6]} />
|
<RepairGame mission="ebike" position={[42.2399, 4.5484, 34.6468]} />
|
||||||
<RepairGame mission="pylone" position={[64, 0, -66]} />
|
<RepairGame mission="pylon" position={[64, 0, -66]} />
|
||||||
<RepairGame mission="ferme" position={[-24, 0, 42]} />
|
<RepairGame mission="farm" position={[-24, 0, 42]} />
|
||||||
```
|
```
|
||||||
|
|
||||||
Only the repair game whose `mission` matches `useGameStore().mainState` renders active content.
|
Only the repair game whose `mission` matches `useGameStore().mainState` renders active content.
|
||||||
|
|||||||
+17
-17
@@ -65,18 +65,18 @@ Main states:
|
|||||||
| Main state | Role |
|
| Main state | Role |
|
||||||
| ---------- | ------------------------------- |
|
| ---------- | ------------------------------- |
|
||||||
| `intro` | Onboarding and opening sequence |
|
| `intro` | Onboarding and opening sequence |
|
||||||
| `bike` | E-bike repair sequence |
|
| `ebike` | E-bike repair sequence |
|
||||||
| `pylone` | Power pylon repair sequence |
|
| `pylon` | Power pylon repair sequence |
|
||||||
| `ferme` | Vertical farm repair sequence |
|
| `farm` | Vertical farm repair sequence |
|
||||||
| `outro` | Ending sequence |
|
| `outro` | Ending sequence |
|
||||||
|
|
||||||
Other important state:
|
Other important state:
|
||||||
|
|
||||||
- `isCinematicPlaying`
|
- `isCinematicPlaying`
|
||||||
- `intro`
|
- `intro`
|
||||||
- `bike`
|
- `ebike`
|
||||||
- `pylone`
|
- `pylon`
|
||||||
- `ferme`
|
- `farm`
|
||||||
- `outro`
|
- `outro`
|
||||||
|
|
||||||
Mission steps:
|
Mission steps:
|
||||||
@@ -125,28 +125,28 @@ For development and debug tooling, direct setters also exist:
|
|||||||
```ts
|
```ts
|
||||||
const setMainState = useGameStore((state) => state.setMainState);
|
const setMainState = useGameStore((state) => state.setMainState);
|
||||||
|
|
||||||
setMainState("bike");
|
setMainState("ebike");
|
||||||
```
|
```
|
||||||
|
|
||||||
Direct setters are useful for debug panels, but production gameplay should prefer business actions such as:
|
Direct setters are useful for debug panels, but production gameplay should prefer business actions such as:
|
||||||
|
|
||||||
- `advanceGameState`
|
- `advanceGameState`
|
||||||
- `completeBike`
|
- `completeEbike`
|
||||||
- `completePylone`
|
- `completePylon`
|
||||||
- `completeFerme`
|
- `completeFarm`
|
||||||
- `completeMission`
|
- `completeMission`
|
||||||
|
|
||||||
Mission gameplay that can target `bike`, `pylone`, or `ferme` should prefer generic mission actions:
|
Mission gameplay that can target `ebike`, `pylon`, or `farm` should prefer generic mission actions:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const setMissionStep = useGameStore((state) => state.setMissionStep);
|
const setMissionStep = useGameStore((state) => state.setMissionStep);
|
||||||
const completeMission = useGameStore((state) => state.completeMission);
|
const completeMission = useGameStore((state) => state.completeMission);
|
||||||
|
|
||||||
setMissionStep("bike", "inspected");
|
setMissionStep("ebike", "inspected");
|
||||||
completeMission("bike");
|
completeMission("ebike");
|
||||||
```
|
```
|
||||||
|
|
||||||
This keeps reusable gameplay components such as `RepairGame` from duplicating mission-specific branches like `setBikeState`, `setPyloneState`, and `setFermeState`.
|
This keeps reusable gameplay components such as `RepairGame` from duplicating mission-specific branches like `setEbikeState`, `setPylonState`, and `setFarmState`.
|
||||||
|
|
||||||
## Settings Store
|
## Settings Store
|
||||||
|
|
||||||
@@ -188,9 +188,9 @@ State/actions:
|
|||||||
Current production repair placement:
|
Current production repair placement:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<RepairGame mission="bike" position={[8, 0, -6]} />
|
<RepairGame mission="ebike" position={[42.2399, 4.5484, 34.6468]} />
|
||||||
<RepairGame mission="pylone" position={[64, 0, -66]} />
|
<RepairGame mission="pylon" position={[64, 0, -66]} />
|
||||||
<RepairGame mission="ferme" position={[-24, 0, 42]} />
|
<RepairGame mission="farm" position={[-24, 0, 42]} />
|
||||||
```
|
```
|
||||||
|
|
||||||
`RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep` and `completeMission`.
|
`RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep` and `completeMission`.
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ This document lists the user-visible and developer-facing features implemented i
|
|||||||
|
|
||||||
## Repair Gameplay
|
## Repair Gameplay
|
||||||
|
|
||||||
- Reusable `RepairGame` mounted for `bike`, `pylone`, and `ferme`
|
- Reusable `RepairGame` mounted for `ebike`, `pylon`, and `farm`
|
||||||
- Mission progression driven by Zustand and shared `MissionStep` types
|
- Mission progression driven by Zustand and shared `MissionStep` types
|
||||||
- Production repair positions:
|
- Production repair positions:
|
||||||
- `bike` at `[8, 0, -6]`
|
- `ebike` at `[42.2399, 4.5484, 34.6468]`
|
||||||
- `pylone` at `[64, 0, -66]`
|
- `pylon` at `[64, 0, -66]`
|
||||||
- `ferme` at `[-24, 0, 42]`
|
- `farm` at `[-24, 0, 42]`
|
||||||
- Debug physics repair playground zones for all three missions
|
- Debug physics repair playground zones for all three missions
|
||||||
- Data-driven mission config in `src/data/gameplay/repairMissions.ts`
|
- Data-driven mission config in `src/data/gameplay/repairMissions.ts`
|
||||||
- Mission flow: `locked -> waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done`
|
- Mission flow: `locked -> waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done`
|
||||||
@@ -95,7 +95,7 @@ This document lists the user-visible and developer-facing features implemented i
|
|||||||
## Game Progression Store
|
## Game Progression Store
|
||||||
|
|
||||||
- Zustand `useGameStore` for durable gameplay progression
|
- Zustand `useGameStore` for durable gameplay progression
|
||||||
- Main states: `intro`, `bike`, `pylone`, `ferme`, `outro`
|
- Main states: `intro`, `ebike`, `pylon`, `farm`, `outro`
|
||||||
- Per-mission repair step state
|
- Per-mission repair step state
|
||||||
- Per-mission completion flags
|
- Per-mission completion flags
|
||||||
- Generic mission helpers: `setMissionStep`, `completeMission`, `advanceGameState`, `rewindGameState`, `resetGame`
|
- Generic mission helpers: `setMissionStep`, `completeMission`, `advanceGameState`, `rewindGameState`, `resetGame`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ The main feature is a reusable repair flow mounted in the production game scene.
|
|||||||
|
|
||||||
The current user flow is:
|
The current user flow is:
|
||||||
|
|
||||||
1. Enter a mission state such as `bike`, `pylone`, or `ferme`.
|
1. Enter a mission state such as `ebike`, `pylon`, or `farm`.
|
||||||
2. Move close to the active repair object in the game scene.
|
2. Move close to the active repair object in the game scene.
|
||||||
3. Aim at the object and press the interaction key when prompted.
|
3. Aim at the object and press the interaction key when prompted.
|
||||||
4. The mission step moves from `waiting` to `inspected`.
|
4. The mission step moves from `waiting` to `inspected`.
|
||||||
@@ -21,7 +21,7 @@ The current user flow is:
|
|||||||
11. Move each scanned broken part into a compatible placeholder so the damaged parts are stored in the case.
|
11. Move each scanned broken part into a compatible placeholder so the damaged parts are stored in the case.
|
||||||
12. Press `E` on the green install target to move to `reassembling`. Wrong parts turn the target red and cannot finish the repair.
|
12. Press `E` on the green install target to move to `reassembling`. Wrong parts turn the target red and cannot finish the repair.
|
||||||
13. The exploded object animates back into its assembled form with completion particles, then moves to `done`.
|
13. The exploded object animates back into its assembled form with completion particles, then moves to `done`.
|
||||||
14. Press `E` on the completion target. The repair case closes, returns to the ground, disappears, then `completeMission` moves to the next mission or to `outro` after `ferme`.
|
14. Press `E` on the completion target. The repair case closes, returns to the ground, disappears, then `completeMission` moves to the next mission or to `outro` after `farm`.
|
||||||
|
|
||||||
## Why It Matters
|
## Why It Matters
|
||||||
|
|
||||||
@@ -39,11 +39,11 @@ In `inspected`, `RepairGame` can also move to `fragmented`. Keyboard input goes
|
|||||||
|
|
||||||
In `fragmented`, the repair object is rendered with `ExplodableModel`, then automatically advances to `scanning`. In `scanning`, the exploded model remains visible, a blue scan visual moves from part to part, and a red halo/wire marker plus the configured broken UI video stay attached to configured broken parts after the scanner reaches them. The scan matches configured broken parts by `nodeName` and reports diagnostics when a configured node is missing. In `repairing`, the case opens in a larger focused transform, `RepairCaseModel` traverses the case GLTF for empty nodes named `placeholder_*`, several grabbable replacement parts appear on those slot positions, and releasing a part near a slot snaps it into place with a short GSAP animation. Scanned broken parts are also rendered as grabbable objects and must be deposited into a compatible slot before the final install target validates. If `brokenParts[].caseSlotName` is configured, that broken part snaps only to the matching slot; otherwise it can use any available slot. If the current case asset has no slot nodes, the flow keeps using fallback focus positions and logs the fallback. Replacement parts show green or red placement feedback after snapping, broken parts show stored feedback after deposit, and the install target gives a short blocked feedback if the player tries to validate too early. The install target only validates when the configured correct replacement part is placed and all scanned broken parts have been deposited. In `reassembling`, the exploded model animates back into its assembled position with green completion particles before the flow moves to `done`. In `done`, the repaired object remains visible with a completion target; validating closes the repair case first, then plays the case exit animation before advancing the global mission progression.
|
In `fragmented`, the repair object is rendered with `ExplodableModel`, then automatically advances to `scanning`. In `scanning`, the exploded model remains visible, a blue scan visual moves from part to part, and a red halo/wire marker plus the configured broken UI video stay attached to configured broken parts after the scanner reaches them. The scan matches configured broken parts by `nodeName` and reports diagnostics when a configured node is missing. In `repairing`, the case opens in a larger focused transform, `RepairCaseModel` traverses the case GLTF for empty nodes named `placeholder_*`, several grabbable replacement parts appear on those slot positions, and releasing a part near a slot snaps it into place with a short GSAP animation. Scanned broken parts are also rendered as grabbable objects and must be deposited into a compatible slot before the final install target validates. If `brokenParts[].caseSlotName` is configured, that broken part snaps only to the matching slot; otherwise it can use any available slot. If the current case asset has no slot nodes, the flow keeps using fallback focus positions and logs the fallback. Replacement parts show green or red placement feedback after snapping, broken parts show stored feedback after deposit, and the install target gives a short blocked feedback if the player tries to validate too early. The install target only validates when the configured correct replacement part is placed and all scanned broken parts have been deposited. In `reassembling`, the exploded model animates back into its assembled position with green completion particles before the flow moves to `done`. In `done`, the repaired object remains visible with a completion target; validating closes the repair case first, then plays the case exit animation before advancing the global mission progression.
|
||||||
|
|
||||||
The mission config now carries the mission-specific variations. `bike` repairs one cooling core, `pylone` scans and stores both the lamp relay and a damaged panel with slower scan/reassembly timing, and `ferme` scans and stores an irrigation pump plus humidity sensor with faster scan/reassembly timing.
|
The mission config now carries the mission-specific variations. `ebike` repairs one cooling core, `pylon` scans and stores both the lamp relay and a damaged panel with slower scan/reassembly timing, and `farm` scans and stores an irrigation pump plus humidity sensor with faster scan/reassembly timing.
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- `src/world/GameStageContent.tsx` mounts production `RepairGame` instances for `bike`, `pylone`, and `ferme`.
|
- `src/world/GameStageContent.tsx` mounts production `RepairGame` instances for `ebike`, `pylon`, and `farm`.
|
||||||
- `src/components/three/gameplay/RepairCompletionStep.tsx` renders the final repaired object, completion target, case exit animation, and mission UI prompt.
|
- `src/components/three/gameplay/RepairCompletionStep.tsx` renders the final repaired object, completion target, case exit animation, and mission UI prompt.
|
||||||
- `src/components/three/gameplay/RepairGame.tsx` composes the reusable production repair flow.
|
- `src/components/three/gameplay/RepairGame.tsx` composes the reusable production repair flow.
|
||||||
- `src/components/three/gameplay/RepairBrokenPartHighlight.tsx` renders the red halo and wire marker around detected broken parts during scanning.
|
- `src/components/three/gameplay/RepairBrokenPartHighlight.tsx` renders the red halo and wire marker around detected broken parts during scanning.
|
||||||
@@ -65,7 +65,7 @@ The mission config now carries the mission-specific variations. `bike` repairs o
|
|||||||
- `src/components/three/models/ExplodableModel.tsx` renders selectable models with split/exploded visualization.
|
- `src/components/three/models/ExplodableModel.tsx` renders selectable models with split/exploded visualization.
|
||||||
- `src/data/gameplay/repairCaseConfig.ts` stores repair case model, sound, and animation constants.
|
- `src/data/gameplay/repairCaseConfig.ts` stores repair case model, sound, and animation constants.
|
||||||
- `src/data/gameplay/repairGameConfig.ts` stores repair flow timing constants.
|
- `src/data/gameplay/repairGameConfig.ts` stores repair flow timing constants.
|
||||||
- `src/data/gameplay/repairMissions.ts` stores reusable repair mission config for `bike`, `pylone`, and `ferme`.
|
- `src/data/gameplay/repairMissions.ts` stores reusable repair mission config for `ebike`, `pylon`, and `farm`.
|
||||||
- `src/managers/stores/useGameStore.ts` stores mission progression state and generic mission step helpers.
|
- `src/managers/stores/useGameStore.ts` stores mission progression state and generic mission step helpers.
|
||||||
- `src/types/gameplay/repairMission.ts` contains shared repair mission ids, mission steps, and guards used by the store, data config, debug UI, and gameplay components.
|
- `src/types/gameplay/repairMission.ts` contains shared repair mission ids, mission steps, and guards used by the store, data config, debug UI, and gameplay components.
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ The mission config now carries the mission-specific variations. `bike` repairs o
|
|||||||
|
|
||||||
The production repair flow currently requires:
|
The production repair flow currently requires:
|
||||||
|
|
||||||
- the active `mainState` to be one of `bike`, `pylone`, or `ferme`
|
- the active `mainState` to be one of `ebike`, `pylon`, or `farm`
|
||||||
- `GameStageContent` mounted inside the game scene Rapier `Physics` boundary
|
- `GameStageContent` mounted inside the game scene Rapier `Physics` boundary
|
||||||
- model assets available under `public/models/`
|
- model assets available under `public/models/`
|
||||||
- sound assets available under `public/sounds/`
|
- sound assets available under `public/sounds/`
|
||||||
|
|||||||
+141
-26
@@ -26,6 +26,32 @@ const IDENTITY_NODE = {
|
|||||||
scale: [1, 1, 1],
|
scale: [1, 1, 1],
|
||||||
};
|
};
|
||||||
const MAX_MESH_Y_PLACEMENT_OFFSET = 2;
|
const MAX_MESH_Y_PLACEMENT_OFFSET = 2;
|
||||||
|
const RAW_INDEX = {
|
||||||
|
directionGroup: 5,
|
||||||
|
fermeGroup: 4798,
|
||||||
|
energieGroup: 4800,
|
||||||
|
lafabrikGroup: 4873,
|
||||||
|
ecoleGroup: 4895,
|
||||||
|
residenceZoneSources: [830, 874, 892],
|
||||||
|
};
|
||||||
|
const RAW_RANGES = {
|
||||||
|
directionPrimary: [6, 12],
|
||||||
|
residenceZone1: [831, 873],
|
||||||
|
residenceZone2: [875, 891],
|
||||||
|
residenceZone3: [893, 942],
|
||||||
|
residenceBikes: [14, 23],
|
||||||
|
residenceMailboxes: [25, 58],
|
||||||
|
energyPylones: [61, 96],
|
||||||
|
vegetationPrimary: [98, 829],
|
||||||
|
lakePipes: [944, 944],
|
||||||
|
fields: [946, 4594],
|
||||||
|
farm: [4595, 4799],
|
||||||
|
vegetationFarmArea: [4750, 4797],
|
||||||
|
energy: [4801, 4872],
|
||||||
|
lafabrik: [4874, 4894],
|
||||||
|
directionSecondary: [4896, 4897],
|
||||||
|
vegetationSecondary: [4898, 4997],
|
||||||
|
};
|
||||||
|
|
||||||
function cloneNode(node) {
|
function cloneNode(node) {
|
||||||
return {
|
return {
|
||||||
@@ -69,6 +95,15 @@ function getOrCreateModelGroup(parent, modelName) {
|
|||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRequiredRawNode(rawData, index, label) {
|
||||||
|
const node = rawData[index];
|
||||||
|
if (!node) {
|
||||||
|
throw new Error(`Missing raw map node for ${label} at index ${index}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
function createRenderableObject(objectNode, meshNode) {
|
function createRenderableObject(objectNode, meshNode) {
|
||||||
const mappedMesh = mapMeshNode(meshNode);
|
const mappedMesh = mapMeshNode(meshNode);
|
||||||
const renderableNode = cloneNode(objectNode ?? meshNode);
|
const renderableNode = cloneNode(objectNode ?? meshNode);
|
||||||
@@ -177,28 +212,53 @@ function getNearestGroup(groups, node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createResidenceZones(rawData, residence) {
|
function createResidenceZones(rawData, residence) {
|
||||||
const zoneSources = [rawData[830], rawData[874], rawData[892]];
|
const zoneSources = RAW_INDEX.residenceZoneSources.map((index) =>
|
||||||
|
getRequiredRawNode(rawData, index, `residence zone ${index}`),
|
||||||
|
);
|
||||||
const zones = zoneSources.map((sourceNode, index) => {
|
const zones = zoneSources.map((sourceNode, index) => {
|
||||||
const zone = createGroup(`zone${index + 1}_residence`, sourceNode);
|
const zone = createGroup(`zone${index + 1}_residence`, sourceNode);
|
||||||
residence.children.push(zone);
|
residence.children.push(zone);
|
||||||
return zone;
|
return zone;
|
||||||
});
|
});
|
||||||
|
|
||||||
addBuildingsByRange(rawData, zones[0], 831, 873);
|
addBuildingsByRange(rawData, zones[0], ...RAW_RANGES.residenceZone1);
|
||||||
addBuildingsByRange(rawData, zones[1], 875, 891);
|
addBuildingsByRange(rawData, zones[1], ...RAW_RANGES.residenceZone2);
|
||||||
addBuildingsByRange(rawData, zones[2], 893, 942);
|
addBuildingsByRange(rawData, zones[2], ...RAW_RANGES.residenceZone3);
|
||||||
addObjectsByRange(rawData, zones[0], 831, 873, RESIDENCE_MESH_NAMES);
|
addObjectsByRange(
|
||||||
addObjectsByRange(rawData, zones[1], 875, 891, RESIDENCE_MESH_NAMES);
|
rawData,
|
||||||
addObjectsByRange(rawData, zones[2], 893, 942, RESIDENCE_MESH_NAMES);
|
zones[0],
|
||||||
|
...RAW_RANGES.residenceZone1,
|
||||||
|
RESIDENCE_MESH_NAMES,
|
||||||
|
);
|
||||||
|
addObjectsByRange(
|
||||||
|
rawData,
|
||||||
|
zones[1],
|
||||||
|
...RAW_RANGES.residenceZone2,
|
||||||
|
RESIDENCE_MESH_NAMES,
|
||||||
|
);
|
||||||
|
addObjectsByRange(
|
||||||
|
rawData,
|
||||||
|
zones[2],
|
||||||
|
...RAW_RANGES.residenceZone3,
|
||||||
|
RESIDENCE_MESH_NAMES,
|
||||||
|
);
|
||||||
|
|
||||||
for (let i = 14; i <= 23; i++) {
|
for (
|
||||||
|
let i = RAW_RANGES.residenceBikes[0];
|
||||||
|
i <= RAW_RANGES.residenceBikes[1];
|
||||||
|
i++
|
||||||
|
) {
|
||||||
const node = rawData[i];
|
const node = rawData[i];
|
||||||
if (node?.type === "Mesh" && node.name === "parcebike") {
|
if (node?.type === "Mesh" && node.name === "parcebike") {
|
||||||
addRenderable(getNearestGroup(zones, node), null, node);
|
addRenderable(getNearestGroup(zones, node), null, node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 25; i <= 58; i++) {
|
for (
|
||||||
|
let i = RAW_RANGES.residenceMailboxes[0];
|
||||||
|
i <= RAW_RANGES.residenceMailboxes[1];
|
||||||
|
i++
|
||||||
|
) {
|
||||||
const node = rawData[i];
|
const node = rawData[i];
|
||||||
if (node?.type === "Mesh" && node.name === "boitesauxlettres") {
|
if (node?.type === "Mesh" && node.name === "boitesauxlettres") {
|
||||||
addRenderable(getNearestGroup(zones, node), null, node);
|
addRenderable(getNearestGroup(zones, node), null, node);
|
||||||
@@ -266,12 +326,27 @@ function transformMap() {
|
|||||||
const vegetation = createGroup("vegetation");
|
const vegetation = createGroup("vegetation");
|
||||||
const agriculture = createGroup("agriculture");
|
const agriculture = createGroup("agriculture");
|
||||||
const champs = createGroup("champs");
|
const champs = createGroup("champs");
|
||||||
const ferme = createGroup("ferme", rawData[4798]);
|
const ferme = createGroup(
|
||||||
|
"ferme",
|
||||||
|
getRequiredRawNode(rawData, RAW_INDEX.fermeGroup, "ferme group"),
|
||||||
|
);
|
||||||
const residence = createGroup("residence");
|
const residence = createGroup("residence");
|
||||||
const energie = createGroup("energie", rawData[4800]);
|
const energie = createGroup(
|
||||||
const direction = createGroup("direction", rawData[5]);
|
"energie",
|
||||||
const lafabrik = createGroup("lafabrik", rawData[4873]);
|
getRequiredRawNode(rawData, RAW_INDEX.energieGroup, "energie group"),
|
||||||
const ecole = createGroup("ecole", rawData[4895]);
|
);
|
||||||
|
const direction = createGroup(
|
||||||
|
"direction",
|
||||||
|
getRequiredRawNode(rawData, RAW_INDEX.directionGroup, "direction group"),
|
||||||
|
);
|
||||||
|
const lafabrik = createGroup(
|
||||||
|
"lafabrik",
|
||||||
|
getRequiredRawNode(rawData, RAW_INDEX.lafabrikGroup, "lafabrik group"),
|
||||||
|
);
|
||||||
|
const ecole = createGroup(
|
||||||
|
"ecole",
|
||||||
|
getRequiredRawNode(rawData, RAW_INDEX.ecoleGroup, "ecole group"),
|
||||||
|
);
|
||||||
delete ecole.role;
|
delete ecole.role;
|
||||||
const unclassified = createGroup("unclassified");
|
const unclassified = createGroup("unclassified");
|
||||||
|
|
||||||
@@ -288,20 +363,60 @@ function transformMap() {
|
|||||||
);
|
);
|
||||||
agriculture.children.push(champs, ferme);
|
agriculture.children.push(champs, ferme);
|
||||||
|
|
||||||
addObjectsByRange(rawData, direction, 6, 12, DIRECTION_MESH_NAMES);
|
addObjectsByRange(
|
||||||
|
rawData,
|
||||||
|
direction,
|
||||||
|
...RAW_RANGES.directionPrimary,
|
||||||
|
DIRECTION_MESH_NAMES,
|
||||||
|
);
|
||||||
addStandaloneObject(rawData, residence, "ebike");
|
addStandaloneObject(rawData, residence, "ebike");
|
||||||
createResidenceZones(rawData, residence);
|
createResidenceZones(rawData, residence);
|
||||||
addObjectsByRange(rawData, energie, 61, 96, new Set(["pyloneelectrique"]));
|
addObjectsByRange(
|
||||||
addObjectsByRange(rawData, vegetation, 98, 829, VEGETATION_MESH_NAMES);
|
rawData,
|
||||||
addObjectsByRange(rawData, agriculture, 944, 944, new Set(["tuyauxlac"]));
|
energie,
|
||||||
addObjectsByRange(rawData, champs, 946, 4594, CHAMP_MESH_NAMES);
|
...RAW_RANGES.energyPylones,
|
||||||
addObjectsByRange(rawData, ferme, 4595, 4799, FERME_MESH_NAMES);
|
new Set(["pyloneelectrique"]),
|
||||||
addObjectsByRange(rawData, vegetation, 4750, 4797, VEGETATION_MESH_NAMES);
|
);
|
||||||
addObjectsByRange(rawData, energie, 4801, 4872, ENERGIE_MESH_NAMES);
|
addObjectsByRange(
|
||||||
addBuildingsByRange(rawData, lafabrik, 4874, 4894);
|
rawData,
|
||||||
addObjectsByRange(rawData, lafabrik, 4874, 4894, LAFABRIK_MESH_NAMES);
|
vegetation,
|
||||||
addObjectsByRange(rawData, direction, 4896, 4897, DIRECTION_MESH_NAMES);
|
...RAW_RANGES.vegetationPrimary,
|
||||||
addObjectsByRange(rawData, vegetation, 4898, 4997, VEGETATION_MESH_NAMES);
|
VEGETATION_MESH_NAMES,
|
||||||
|
);
|
||||||
|
addObjectsByRange(
|
||||||
|
rawData,
|
||||||
|
agriculture,
|
||||||
|
...RAW_RANGES.lakePipes,
|
||||||
|
new Set(["tuyauxlac"]),
|
||||||
|
);
|
||||||
|
addObjectsByRange(rawData, champs, ...RAW_RANGES.fields, CHAMP_MESH_NAMES);
|
||||||
|
addObjectsByRange(rawData, ferme, ...RAW_RANGES.farm, FERME_MESH_NAMES);
|
||||||
|
addObjectsByRange(
|
||||||
|
rawData,
|
||||||
|
vegetation,
|
||||||
|
...RAW_RANGES.vegetationFarmArea,
|
||||||
|
VEGETATION_MESH_NAMES,
|
||||||
|
);
|
||||||
|
addObjectsByRange(rawData, energie, ...RAW_RANGES.energy, ENERGIE_MESH_NAMES);
|
||||||
|
addBuildingsByRange(rawData, lafabrik, ...RAW_RANGES.lafabrik);
|
||||||
|
addObjectsByRange(
|
||||||
|
rawData,
|
||||||
|
lafabrik,
|
||||||
|
...RAW_RANGES.lafabrik,
|
||||||
|
LAFABRIK_MESH_NAMES,
|
||||||
|
);
|
||||||
|
addObjectsByRange(
|
||||||
|
rawData,
|
||||||
|
direction,
|
||||||
|
...RAW_RANGES.directionSecondary,
|
||||||
|
DIRECTION_MESH_NAMES,
|
||||||
|
);
|
||||||
|
addObjectsByRange(
|
||||||
|
rawData,
|
||||||
|
vegetation,
|
||||||
|
...RAW_RANGES.vegetationSecondary,
|
||||||
|
VEGETATION_MESH_NAMES,
|
||||||
|
);
|
||||||
|
|
||||||
for (let i = 0; i < rawData.length; i++) {
|
for (let i = 0; i < rawData.length; i++) {
|
||||||
const node = rawData[i];
|
const node = rawData[i];
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export function GameFlow(): null {
|
|||||||
const setStep = useGameStore((state) => state.setIntroStep);
|
const setStep = useGameStore((state) => state.setIntroStep);
|
||||||
const setActivityCity = useGameStore((state) => state.setActivityCity);
|
const setActivityCity = useGameStore((state) => state.setActivityCity);
|
||||||
const setCanMove = useGameStore((state) => state.setCanMove);
|
const setCanMove = useGameStore((state) => state.setCanMove);
|
||||||
|
const completeIntro = useGameStore((state) => state.completeIntro);
|
||||||
const hasInitialized = useRef(false);
|
const hasInitialized = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,10 +56,17 @@ export function GameFlow(): null {
|
|||||||
|
|
||||||
if (step === "manipulation") {
|
if (step === "manipulation") {
|
||||||
setCanMove(false);
|
setCanMove(false);
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
completeIntro();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [step, setStep, setActivityCity, setCanMove]);
|
}, [completeIntro, step, setStep, setActivityCity, setCanMove]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,27 @@ type InteractableObjectProps =
|
|||||||
const _cameraPos = new THREE.Vector3();
|
const _cameraPos = new THREE.Vector3();
|
||||||
const _cameraDir = new THREE.Vector3();
|
const _cameraDir = new THREE.Vector3();
|
||||||
const _objectPos = new THREE.Vector3();
|
const _objectPos = new THREE.Vector3();
|
||||||
|
const _objectBounds = new THREE.Box3();
|
||||||
const _raycaster = new THREE.Raycaster();
|
const _raycaster = new THREE.Raycaster();
|
||||||
|
|
||||||
|
function getInteractableWorldPosition(
|
||||||
|
group: THREE.Group,
|
||||||
|
debugSphere: THREE.Mesh | null,
|
||||||
|
): THREE.Vector3 {
|
||||||
|
_objectBounds.makeEmpty();
|
||||||
|
|
||||||
|
for (const child of group.children) {
|
||||||
|
if (child === debugSphere) continue;
|
||||||
|
_objectBounds.expandByObject(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_objectBounds.isEmpty()) {
|
||||||
|
return _objectBounds.getCenter(_objectPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return group.getWorldPosition(_objectPos);
|
||||||
|
}
|
||||||
|
|
||||||
function createInteractableHandle(
|
function createInteractableHandle(
|
||||||
props: InteractableObjectProps,
|
props: InteractableObjectProps,
|
||||||
): InteractableHandle {
|
): InteractableHandle {
|
||||||
@@ -158,7 +177,7 @@ export function InteractableObject(
|
|||||||
const t = bodyRef.current.translation();
|
const t = bodyRef.current.translation();
|
||||||
_objectPos.set(t.x, t.y, t.z);
|
_objectPos.set(t.x, t.y, t.z);
|
||||||
} else if (group) {
|
} else if (group) {
|
||||||
group.getWorldPosition(_objectPos);
|
getInteractableWorldPosition(group, debugSphereRef.current);
|
||||||
} else {
|
} else {
|
||||||
_objectPos.set(...position);
|
_objectPos.set(...position);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useEffect, useMemo } from "react";
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three";
|
import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three";
|
||||||
import { disposeObject3D } from "@/utils/three/dispose";
|
|
||||||
|
|
||||||
function applyShadowSettings(
|
function applyShadowSettings(
|
||||||
object: THREE.Object3D,
|
object: THREE.Object3D,
|
||||||
@@ -48,12 +47,6 @@ export function SimpleModel({
|
|||||||
applyShadowSettings(model, castShadow, receiveShadow);
|
applyShadowSettings(model, castShadow, receiveShadow);
|
||||||
}, [castShadow, model, receiveShadow]);
|
}, [castShadow, model, receiveShadow]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
disposeObject3D(model);
|
|
||||||
};
|
|
||||||
}, [model]);
|
|
||||||
|
|
||||||
const parsedScale =
|
const parsedScale =
|
||||||
typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale;
|
typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { RotateCcw, StepBack, StepForward } from "lucide-react";
|
import { RotateCcw, StepBack, StepForward } from "lucide-react";
|
||||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
|
||||||
import { isMissionStep, MISSION_STEPS } from "@/types/gameplay/repairMission";
|
|
||||||
import {
|
import {
|
||||||
GAME_STEPS,
|
GAME_STEPS,
|
||||||
isGameStep,
|
isGameStep,
|
||||||
MAIN_GAME_STATES,
|
MAIN_GAME_STATES,
|
||||||
type MainGameState,
|
} from "@/data/game/gameStateConfig";
|
||||||
} from "@/types/game";
|
import {
|
||||||
|
isMissionStep,
|
||||||
|
MISSION_STEPS,
|
||||||
|
} from "@/data/gameplay/repairMissionState";
|
||||||
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
|
import type { MainGameState } from "@/types/game";
|
||||||
|
|
||||||
function toPascalCase(value: string): string {
|
function toPascalCase(value: string): string {
|
||||||
return value
|
return value
|
||||||
@@ -19,18 +22,18 @@ function toPascalCase(value: string): string {
|
|||||||
export function GameStateDebugPanel(): React.JSX.Element {
|
export function GameStateDebugPanel(): React.JSX.Element {
|
||||||
const mainState = useGameStore((state) => state.mainState);
|
const mainState = useGameStore((state) => state.mainState);
|
||||||
const ebikeStep = useGameStore((state) => state.ebike.currentStep);
|
const ebikeStep = useGameStore((state) => state.ebike.currentStep);
|
||||||
const pyloneStep = useGameStore((state) => state.pylone.currentStep);
|
const pylonStep = useGameStore((state) => state.pylon.currentStep);
|
||||||
const fermeStep = useGameStore((state) => state.ferme.currentStep);
|
const farmStep = useGameStore((state) => state.farm.currentStep);
|
||||||
const detail = useGameStore((state) => {
|
const detail = useGameStore((state) => {
|
||||||
switch (state.mainState) {
|
switch (state.mainState) {
|
||||||
case "intro":
|
case "intro":
|
||||||
return state.intro.currentStep;
|
return state.intro.currentStep;
|
||||||
case "ebike":
|
case "ebike":
|
||||||
return state.ebike.currentStep;
|
return state.ebike.currentStep;
|
||||||
case "pylone":
|
case "pylon":
|
||||||
return state.pylone.currentStep;
|
return state.pylon.currentStep;
|
||||||
case "ferme":
|
case "farm":
|
||||||
return state.ferme.currentStep;
|
return state.farm.currentStep;
|
||||||
case "outro":
|
case "outro":
|
||||||
return state.outro.hasStarted ? "started" : "waiting";
|
return state.outro.hasStarted ? "started" : "waiting";
|
||||||
}
|
}
|
||||||
@@ -38,8 +41,8 @@ export function GameStateDebugPanel(): React.JSX.Element {
|
|||||||
const setMainState = useGameStore((state) => state.setMainState);
|
const setMainState = useGameStore((state) => state.setMainState);
|
||||||
const setIntroStep = useGameStore((state) => state.setIntroStep);
|
const setIntroStep = useGameStore((state) => state.setIntroStep);
|
||||||
const setEbikeState = useGameStore((state) => state.setEbikeState);
|
const setEbikeState = useGameStore((state) => state.setEbikeState);
|
||||||
const setPyloneState = useGameStore((state) => state.setPyloneState);
|
const setPylonState = useGameStore((state) => state.setPylonState);
|
||||||
const setFermeState = useGameStore((state) => state.setFermeState);
|
const setFarmState = useGameStore((state) => state.setFarmState);
|
||||||
const setOutroState = useGameStore((state) => state.setOutroState);
|
const setOutroState = useGameStore((state) => state.setOutroState);
|
||||||
const advanceGameState = useGameStore((state) => state.advanceGameState);
|
const advanceGameState = useGameStore((state) => state.advanceGameState);
|
||||||
const rewindGameState = useGameStore((state) => state.rewindGameState);
|
const rewindGameState = useGameStore((state) => state.rewindGameState);
|
||||||
@@ -72,13 +75,13 @@ export function GameStateDebugPanel(): React.JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainState === "pylone") {
|
if (mainState === "pylon") {
|
||||||
setPyloneState({ currentStep: nextSubState });
|
setPylonState({ currentStep: nextSubState });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainState === "ferme") {
|
if (mainState === "farm") {
|
||||||
setFermeState({ currentStep: nextSubState });
|
setFarmState({ currentStep: nextSubState });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,18 +89,34 @@ export function GameStateDebugPanel(): React.JSX.Element {
|
|||||||
function setDebugMainState(nextMainState: MainGameState): void {
|
function setDebugMainState(nextMainState: MainGameState): void {
|
||||||
setMainState(nextMainState);
|
setMainState(nextMainState);
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextMainState === "pylon" ||
|
||||||
|
nextMainState === "farm" ||
|
||||||
|
nextMainState === "outro"
|
||||||
|
) {
|
||||||
|
setEbikeState({ currentStep: "done", isRepaired: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextMainState === "farm" || nextMainState === "outro") {
|
||||||
|
setPylonState({ currentStep: "done", isPowered: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextMainState === "outro") {
|
||||||
|
setFarmState({ currentStep: "done", irrigationFixed: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (nextMainState === "ebike" && ebikeStep === "locked") {
|
if (nextMainState === "ebike" && ebikeStep === "locked") {
|
||||||
setEbikeState({ currentStep: "waiting" });
|
setEbikeState({ currentStep: "waiting" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextMainState === "pylone" && pyloneStep === "locked") {
|
if (nextMainState === "pylon" && pylonStep === "locked") {
|
||||||
setPyloneState({ currentStep: "waiting" });
|
setPylonState({ currentStep: "waiting" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextMainState === "ferme" && fermeStep === "locked") {
|
if (nextMainState === "farm" && farmStep === "locked") {
|
||||||
setFermeState({ currentStep: "waiting" });
|
setFarmState({ currentStep: "waiting" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as THREE from "three";
|
|||||||
import { ZONES } from "@/data/zones";
|
import { ZONES } from "@/data/zones";
|
||||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
import { GAME_STEPS } from "@/types/game";
|
import { GAME_STEPS } from "@/data/game/gameStateConfig";
|
||||||
|
|
||||||
const _playerPos = new THREE.Vector3();
|
const _playerPos = new THREE.Vector3();
|
||||||
const _zonePos = new THREE.Vector3();
|
const _zonePos = new THREE.Vector3();
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { AudioCategory } from "@/managers/AudioManager";
|
||||||
|
|
||||||
export const AUDIO_PATHS = {
|
export const AUDIO_PATHS = {
|
||||||
intro: "/sounds/effect/fa.mp3",
|
intro: "/sounds/effect/fa.mp3",
|
||||||
bienvenue: "/sounds/effect/fa.mp3",
|
bienvenue: "/sounds/effect/fa.mp3",
|
||||||
@@ -5,3 +7,9 @@ export const AUDIO_PATHS = {
|
|||||||
searching: "/sounds/effect/fa.mp3",
|
searching: "/sounds/effect/fa.mp3",
|
||||||
helped: "/sounds/effect/fa.mp3",
|
helped: "/sounds/effect/fa.mp3",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const DEFAULT_CATEGORY_VOLUMES: Record<AudioCategory, number> = {
|
||||||
|
music: 1,
|
||||||
|
sfx: 1,
|
||||||
|
dialogue: 1,
|
||||||
|
};
|
||||||
|
|||||||
@@ -31,19 +31,19 @@ export const TEST_SCENE_REPAIR_ZONES = [
|
|||||||
position: [-12, 0, -12],
|
position: [-12, 0, -12],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mission: "pylone",
|
mission: "pylon",
|
||||||
label: "Pylone",
|
label: "Pylon",
|
||||||
color: "#facc15",
|
color: "#facc15",
|
||||||
position: [0, 0, -12],
|
position: [0, 0, -12],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mission: "ferme",
|
mission: "farm",
|
||||||
label: "Farm",
|
label: "Farm",
|
||||||
color: "#86efac",
|
color: "#86efac",
|
||||||
position: [12, 0, -12],
|
position: [12, 0, -12],
|
||||||
},
|
},
|
||||||
] as const satisfies readonly {
|
] as const satisfies readonly {
|
||||||
mission: "ebike" | "pylone" | "ferme";
|
mission: "ebike" | "pylon" | "farm";
|
||||||
label: string;
|
label: string;
|
||||||
color: string;
|
color: string;
|
||||||
position: Vector3Tuple;
|
position: Vector3Tuple;
|
||||||
|
|||||||
@@ -38,53 +38,59 @@ export const docGroups: DocGroup[] = [
|
|||||||
subtitle: "Gameplay implementation",
|
subtitle: "Gameplay implementation",
|
||||||
meta: "04",
|
meta: "04",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/docs/mission-flow",
|
||||||
|
title: "Mission Flow",
|
||||||
|
subtitle: "Intro and mission progression",
|
||||||
|
meta: "05",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/interaction",
|
path: "/docs/interaction",
|
||||||
title: "Interaction System",
|
title: "Interaction System",
|
||||||
subtitle: "Trigger, grab, hand input",
|
subtitle: "Trigger, grab, hand input",
|
||||||
meta: "05",
|
meta: "06",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/target-architecture",
|
path: "/docs/target-architecture",
|
||||||
title: "Target Architecture",
|
title: "Target Architecture",
|
||||||
subtitle: "Next direction",
|
subtitle: "Next direction",
|
||||||
meta: "06",
|
meta: "07",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/technical-editor",
|
path: "/docs/technical-editor",
|
||||||
title: "Editor Technical Notes",
|
title: "Editor Technical Notes",
|
||||||
subtitle: "Implementation details",
|
subtitle: "Implementation details",
|
||||||
meta: "07",
|
meta: "08",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/audio",
|
path: "/docs/audio",
|
||||||
title: "Audio Technical Notes",
|
title: "Audio Technical Notes",
|
||||||
subtitle: "Music, dialogue, SRT, and SFX",
|
subtitle: "Music, dialogue, SRT, and SFX",
|
||||||
meta: "08",
|
meta: "09",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/hand-tracking",
|
path: "/docs/hand-tracking",
|
||||||
title: "Hand Tracking Technical Notes",
|
title: "Hand Tracking Technical Notes",
|
||||||
subtitle: "Webcam interaction pipeline",
|
subtitle: "Webcam interaction pipeline",
|
||||||
meta: "09",
|
meta: "10",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/zustand",
|
path: "/docs/zustand",
|
||||||
title: "Zustand Stores",
|
title: "Zustand Stores",
|
||||||
subtitle: "Game, settings, subtitles",
|
subtitle: "Game, settings, subtitles",
|
||||||
meta: "10",
|
meta: "11",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/three-debugging",
|
path: "/docs/three-debugging",
|
||||||
title: "Three Debugging",
|
title: "Three Debugging",
|
||||||
subtitle: "Step into Three.js internals",
|
subtitle: "Step into Three.js internals",
|
||||||
meta: "11",
|
meta: "12",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/map-performance",
|
path: "/docs/map-performance",
|
||||||
title: "Map Performance",
|
title: "Map Performance",
|
||||||
subtitle: "Draw calls, triangles, and streaming",
|
subtitle: "Draw calls, triangles, and streaming",
|
||||||
meta: "12",
|
meta: "13",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -95,25 +101,25 @@ export const docGroups: DocGroup[] = [
|
|||||||
path: "/docs/features",
|
path: "/docs/features",
|
||||||
title: "Features",
|
title: "Features",
|
||||||
subtitle: "Implemented scope",
|
subtitle: "Implemented scope",
|
||||||
meta: "13",
|
meta: "14",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/main-feature",
|
path: "/docs/main-feature",
|
||||||
title: "Main Feature",
|
title: "Main Feature",
|
||||||
subtitle: "Repair-game prototype",
|
subtitle: "Repair-game prototype",
|
||||||
meta: "14",
|
meta: "15",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/docs/editor",
|
path: "/docs/editor",
|
||||||
title: "Editor User Guide",
|
title: "Editor User Guide",
|
||||||
subtitle: "Editing workflow",
|
subtitle: "Editing workflow",
|
||||||
meta: "15",
|
meta: "16",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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: "16",
|
meta: "17",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -124,7 +130,7 @@ export const docGroups: DocGroup[] = [
|
|||||||
path: "/docs/code-review",
|
path: "/docs/code-review",
|
||||||
title: "Code Review Prep",
|
title: "Code Review Prep",
|
||||||
subtitle: "Presentation support",
|
subtitle: "Presentation support",
|
||||||
meta: "17",
|
meta: "18",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type { GameStep, MainGameState } from "@/types/game";
|
||||||
|
|
||||||
|
export const GAME_STEPS: readonly GameStep[] = [
|
||||||
|
"intro",
|
||||||
|
"start-intro",
|
||||||
|
"naming",
|
||||||
|
"bienvenue",
|
||||||
|
"star-move",
|
||||||
|
"mission2",
|
||||||
|
"searching",
|
||||||
|
"helped",
|
||||||
|
"manipulation",
|
||||||
|
"outOfFabrik",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MAIN_GAME_STATES: readonly MainGameState[] = [
|
||||||
|
"intro",
|
||||||
|
"ebike",
|
||||||
|
"pylon",
|
||||||
|
"farm",
|
||||||
|
"outro",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const GAME_STEP_VALUES: ReadonlySet<string> = new Set(GAME_STEPS);
|
||||||
|
const MAIN_GAME_STATE_VALUES: ReadonlySet<string> = new Set(MAIN_GAME_STATES);
|
||||||
|
|
||||||
|
export function isGameStep(value: unknown): value is GameStep {
|
||||||
|
return typeof value === "string" && GAME_STEP_VALUES.has(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMainGameState(value: unknown): value is MainGameState {
|
||||||
|
return typeof value === "string" && MAIN_GAME_STATE_VALUES.has(value);
|
||||||
|
}
|
||||||
@@ -1,21 +1,36 @@
|
|||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
import type { RepairMissionId } from "@/types/gameplay/repairMission";
|
import type { RepairMissionId } from "@/types/gameplay/repairMission";
|
||||||
|
|
||||||
|
export interface RepairMissionTriggerConfig {
|
||||||
|
mission: RepairMissionId;
|
||||||
|
label: string;
|
||||||
|
radius: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const EBIKE_REPAIR_POSITION = [
|
export const EBIKE_REPAIR_POSITION = [
|
||||||
42.2399, 4.5484, 34.6468,
|
42.2399, 4.5484, 34.6468,
|
||||||
] as const satisfies Vector3Tuple;
|
] as const satisfies Vector3Tuple;
|
||||||
|
|
||||||
const REPAIR_MISSION_POSITIONS = {
|
const REPAIR_MISSION_POSITIONS = {
|
||||||
ebike: EBIKE_REPAIR_POSITION,
|
ebike: EBIKE_REPAIR_POSITION,
|
||||||
pylone: [64, 0, -66],
|
pylon: [64, 0, -66],
|
||||||
ferme: [-24, 0, 42],
|
farm: [-24, 0, 42],
|
||||||
} as const satisfies Record<RepairMissionId, Vector3Tuple>;
|
} as const satisfies Record<RepairMissionId, Vector3Tuple>;
|
||||||
|
|
||||||
export const REPAIR_MISSION_POSITION_ENTRIES = [
|
export const REPAIR_MISSION_TRIGGERS = [
|
||||||
{ mission: "ebike", position: REPAIR_MISSION_POSITIONS.ebike },
|
{
|
||||||
{ mission: "pylone", position: REPAIR_MISSION_POSITIONS.pylone },
|
mission: "ebike",
|
||||||
{ mission: "ferme", position: REPAIR_MISSION_POSITIONS.ferme },
|
label: "Réparer l'e-bike",
|
||||||
] as const satisfies readonly {
|
radius: 4,
|
||||||
|
},
|
||||||
|
] as const satisfies readonly RepairMissionTriggerConfig[];
|
||||||
|
|
||||||
|
export const REPAIR_MISSION_POSITION_ENTRIES = Object.entries(
|
||||||
|
REPAIR_MISSION_POSITIONS,
|
||||||
|
).map(([mission, position]) => ({
|
||||||
|
mission: mission as RepairMissionId,
|
||||||
|
position,
|
||||||
|
})) satisfies readonly {
|
||||||
mission: RepairMissionId;
|
mission: RepairMissionId;
|
||||||
position: Vector3Tuple;
|
position: Vector3Tuple;
|
||||||
}[];
|
}[];
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import type {
|
||||||
|
MissionStep,
|
||||||
|
RepairMissionId,
|
||||||
|
} from "@/types/gameplay/repairMission";
|
||||||
|
|
||||||
|
const REPAIR_MISSION_IDS = ["ebike", "pylon", "farm"] as const;
|
||||||
|
const REPAIR_MISSION_ID_VALUES: ReadonlySet<string> = new Set(
|
||||||
|
REPAIR_MISSION_IDS,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MISSION_STEPS = [
|
||||||
|
"locked",
|
||||||
|
"waiting",
|
||||||
|
"inspected",
|
||||||
|
"fragmented",
|
||||||
|
"scanning",
|
||||||
|
"repairing",
|
||||||
|
"reassembling",
|
||||||
|
"done",
|
||||||
|
] as const satisfies readonly MissionStep[];
|
||||||
|
const MISSION_STEP_VALUES: ReadonlySet<string> = new Set(MISSION_STEPS);
|
||||||
|
|
||||||
|
export function isRepairMissionId(value: string): value is RepairMissionId {
|
||||||
|
return REPAIR_MISSION_ID_VALUES.has(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMissionStep(value: string): value is MissionStep {
|
||||||
|
return MISSION_STEP_VALUES.has(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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,8 +53,8 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
pylone: {
|
pylon: {
|
||||||
id: "pylone",
|
id: "pylon",
|
||||||
label: "Power pylon",
|
label: "Power pylon",
|
||||||
description:
|
description:
|
||||||
"Restore the pylon lamp relay and damaged panel before reconnecting the grid",
|
"Restore the pylon lamp relay and damaged panel before reconnecting the grid",
|
||||||
@@ -64,17 +64,17 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
|||||||
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
||||||
case: DEFAULT_REPAIR_CASE,
|
case: DEFAULT_REPAIR_CASE,
|
||||||
reassemblySeconds: 1.8,
|
reassemblySeconds: 1.8,
|
||||||
requiredReplacementPartId: "pylone-grid-relay-replacement",
|
requiredReplacementPartId: "pylon-grid-relay-replacement",
|
||||||
scanPartSeconds: 1.4,
|
scanPartSeconds: 1.4,
|
||||||
brokenParts: [
|
brokenParts: [
|
||||||
{
|
{
|
||||||
id: "pylone-grid-relay",
|
id: "pylon-grid-relay",
|
||||||
label: "Grid relay",
|
label: "Grid relay",
|
||||||
nodeName: "lampe",
|
nodeName: "lampe",
|
||||||
caseSlotName: "placeholder_1",
|
caseSlotName: "placeholder_1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pylone-damaged-panel",
|
id: "pylon-damaged-panel",
|
||||||
label: "Damaged solar panel",
|
label: "Damaged solar panel",
|
||||||
nodeName: "panneau2",
|
nodeName: "panneau2",
|
||||||
caseSlotName: "placeholder_2",
|
caseSlotName: "placeholder_2",
|
||||||
@@ -82,24 +82,24 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
|||||||
],
|
],
|
||||||
replacementParts: [
|
replacementParts: [
|
||||||
{
|
{
|
||||||
id: "pylone-grid-relay-replacement",
|
id: "pylon-grid-relay-replacement",
|
||||||
label: "Replacement grid relay",
|
label: "Replacement grid relay",
|
||||||
modelPath: "/models/pylone/model.gltf",
|
modelPath: "/models/pylone/model.gltf",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pylone-stone-distractor",
|
id: "pylon-stone-distractor",
|
||||||
label: "Stone counterweight",
|
label: "Stone counterweight",
|
||||||
modelPath: "/models/galet/model.gltf",
|
modelPath: "/models/galet/model.gltf",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pylone-cooling-distractor",
|
id: "pylon-cooling-distractor",
|
||||||
label: "Cooling core",
|
label: "Cooling core",
|
||||||
modelPath: "/models/refroidisseur/model.gltf",
|
modelPath: "/models/refroidisseur/model.gltf",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ferme: {
|
farm: {
|
||||||
id: "ferme",
|
id: "farm",
|
||||||
label: "Vertical farm",
|
label: "Vertical farm",
|
||||||
description:
|
description:
|
||||||
"Stabilize the irrigation loop and humidity sensor before restarting the farm",
|
"Stabilize the irrigation loop and humidity sensor before restarting the farm",
|
||||||
@@ -109,33 +109,33 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
|||||||
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
||||||
case: DEFAULT_REPAIR_CASE,
|
case: DEFAULT_REPAIR_CASE,
|
||||||
reassemblySeconds: 1.2,
|
reassemblySeconds: 1.2,
|
||||||
requiredReplacementPartId: "ferme-irrigation-pump-replacement",
|
requiredReplacementPartId: "farm-irrigation-pump-replacement",
|
||||||
scanPartSeconds: 0.9,
|
scanPartSeconds: 0.9,
|
||||||
brokenParts: [
|
brokenParts: [
|
||||||
{
|
{
|
||||||
id: "ferme-irrigation-pump",
|
id: "farm-irrigation-pump",
|
||||||
label: "Irrigation pump",
|
label: "Irrigation pump",
|
||||||
caseSlotName: "placeholder_1",
|
caseSlotName: "placeholder_1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ferme-humidity-sensor",
|
id: "farm-humidity-sensor",
|
||||||
label: "Humidity sensor",
|
label: "Humidity sensor",
|
||||||
caseSlotName: "placeholder_2",
|
caseSlotName: "placeholder_2",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
replacementParts: [
|
replacementParts: [
|
||||||
{
|
{
|
||||||
id: "ferme-irrigation-pump-replacement",
|
id: "farm-irrigation-pump-replacement",
|
||||||
label: "Replacement irrigation pump",
|
label: "Replacement irrigation pump",
|
||||||
modelPath: "/models/fermeverticale/model.gltf",
|
modelPath: "/models/fermeverticale/model.gltf",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ferme-tree-distractor",
|
id: "farm-tree-distractor",
|
||||||
label: "Tree sensor",
|
label: "Tree sensor",
|
||||||
modelPath: "/models/sapin/model.gltf",
|
modelPath: "/models/sapin/model.gltf",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ferme-radio-distractor",
|
id: "farm-radio-distractor",
|
||||||
label: "Radio module",
|
label: "Radio module",
|
||||||
modelPath: "/models/talkie/model.gltf",
|
modelPath: "/models/talkie/model.gltf",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
const HAND_TRACKING_LOCAL_WS_URL = "ws://localhost:8000/ws";
|
|
||||||
const HAND_TRACKING_PROD_WS_URL = "wss://handtracking.la-fabrik.fr/ws";
|
|
||||||
|
|
||||||
export const HAND_TRACKING_FRAME_WIDTH = 320;
|
export const HAND_TRACKING_FRAME_WIDTH = 320;
|
||||||
export const HAND_TRACKING_FRAME_HEIGHT = 240;
|
export const HAND_TRACKING_FRAME_HEIGHT = 240;
|
||||||
export const HAND_TRACKING_TARGET_FPS = 10;
|
export const HAND_TRACKING_TARGET_FPS = 10;
|
||||||
@@ -11,15 +8,3 @@ export const HAND_TRACKING_BROWSER_WASM_URL =
|
|||||||
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.35/wasm";
|
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.35/wasm";
|
||||||
export const HAND_TRACKING_BROWSER_MODEL_URL =
|
export const HAND_TRACKING_BROWSER_MODEL_URL =
|
||||||
"https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task";
|
"https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task";
|
||||||
|
|
||||||
export function getHandTrackingWsUrl(): string {
|
|
||||||
const configuredUrl = import.meta.env.VITE_HAND_TRACKING_WS_URL;
|
|
||||||
|
|
||||||
if (configuredUrl) {
|
|
||||||
return configuredUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return import.meta.env.DEV
|
|
||||||
? HAND_TRACKING_LOCAL_WS_URL
|
|
||||||
: HAND_TRACKING_PROD_WS_URL;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -89,13 +89,3 @@ export type MapInstancingAssetType =
|
|||||||
|
|
||||||
export type MapInstancingAssetConfig =
|
export type MapInstancingAssetConfig =
|
||||||
(typeof MAP_INSTANCING_ASSETS)[MapInstancingAssetType];
|
(typeof MAP_INSTANCING_ASSETS)[MapInstancingAssetType];
|
||||||
|
|
||||||
const MAP_INSTANCED_NODE_NAMES: ReadonlySet<string> = new Set(
|
|
||||||
Object.values(MAP_INSTANCING_ASSETS)
|
|
||||||
.filter((config) => config.enabled)
|
|
||||||
.map((config) => config.mapName),
|
|
||||||
);
|
|
||||||
|
|
||||||
export function isInstancedMapNodeName(name: string): boolean {
|
|
||||||
return MAP_INSTANCED_NODE_NAMES.has(name);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
export type MapPerformanceGroupName =
|
||||||
|
| "vegetation"
|
||||||
|
| "crops"
|
||||||
|
| "trees"
|
||||||
|
| "buildings"
|
||||||
|
| "landmarks"
|
||||||
|
| "props"
|
||||||
|
| "terrain"
|
||||||
|
| "sky";
|
||||||
|
|
||||||
|
export type MapPerformanceModelName =
|
||||||
|
| "buisson"
|
||||||
|
| "arbre"
|
||||||
|
| "sapin"
|
||||||
|
| "champdeble"
|
||||||
|
| "champdesoja"
|
||||||
|
| "champsdetournesol"
|
||||||
|
| "ecole"
|
||||||
|
| "generateur"
|
||||||
|
| "fermeverticale"
|
||||||
|
| "lafabrik"
|
||||||
|
| "immeuble1"
|
||||||
|
| "eolienne"
|
||||||
|
| "pylone"
|
||||||
|
| "boiteauxlettres"
|
||||||
|
| "maison1"
|
||||||
|
| "panneauaffichage"
|
||||||
|
| "panneauclassique"
|
||||||
|
| "panneaufleche"
|
||||||
|
| "panneausolaire"
|
||||||
|
| "parcebike"
|
||||||
|
| "terrain"
|
||||||
|
| "sky";
|
||||||
|
|
||||||
|
export const MAP_PERFORMANCE_GROUP_NAMES: readonly MapPerformanceGroupName[] = [
|
||||||
|
"vegetation",
|
||||||
|
"crops",
|
||||||
|
"trees",
|
||||||
|
"buildings",
|
||||||
|
"landmarks",
|
||||||
|
"props",
|
||||||
|
"terrain",
|
||||||
|
"sky",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MAP_PERFORMANCE_MODEL_NAMES: readonly MapPerformanceModelName[] = [
|
||||||
|
"buisson",
|
||||||
|
"arbre",
|
||||||
|
"sapin",
|
||||||
|
"champdeble",
|
||||||
|
"champdesoja",
|
||||||
|
"champsdetournesol",
|
||||||
|
"ecole",
|
||||||
|
"generateur",
|
||||||
|
"fermeverticale",
|
||||||
|
"lafabrik",
|
||||||
|
"immeuble1",
|
||||||
|
"eolienne",
|
||||||
|
"pylone",
|
||||||
|
"boiteauxlettres",
|
||||||
|
"maison1",
|
||||||
|
"panneauaffichage",
|
||||||
|
"panneauclassique",
|
||||||
|
"panneaufleche",
|
||||||
|
"panneausolaire",
|
||||||
|
"parcebike",
|
||||||
|
"terrain",
|
||||||
|
"sky",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MAP_PERFORMANCE_MODEL_GROUPS: Record<
|
||||||
|
MapPerformanceModelName,
|
||||||
|
readonly MapPerformanceGroupName[]
|
||||||
|
> = {
|
||||||
|
buisson: ["vegetation"],
|
||||||
|
arbre: ["vegetation", "trees"],
|
||||||
|
sapin: ["vegetation", "trees"],
|
||||||
|
champdeble: ["vegetation", "crops"],
|
||||||
|
champdesoja: ["vegetation", "crops"],
|
||||||
|
champsdetournesol: ["vegetation", "crops"],
|
||||||
|
ecole: ["buildings", "landmarks"],
|
||||||
|
generateur: ["landmarks"],
|
||||||
|
fermeverticale: ["buildings", "landmarks"],
|
||||||
|
lafabrik: ["buildings", "landmarks"],
|
||||||
|
immeuble1: ["buildings"],
|
||||||
|
eolienne: ["props"],
|
||||||
|
pylone: ["props"],
|
||||||
|
boiteauxlettres: ["props"],
|
||||||
|
maison1: ["buildings"],
|
||||||
|
panneauaffichage: ["props"],
|
||||||
|
panneauclassique: ["props"],
|
||||||
|
panneaufleche: ["props"],
|
||||||
|
panneausolaire: ["props"],
|
||||||
|
parcebike: ["props"],
|
||||||
|
terrain: ["terrain"],
|
||||||
|
sky: ["sky"],
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { SceneLoadingState } from "@/types/world/sceneLoading";
|
||||||
|
|
||||||
|
export const INITIAL_SCENE_LOADING_STATE: SceneLoadingState = {
|
||||||
|
currentStep: "Initialisation du jeu",
|
||||||
|
progress: 0,
|
||||||
|
status: "loading",
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@ export const VEGETATION_TYPES = {
|
|||||||
buissons: {
|
buissons: {
|
||||||
mapName: "buisson",
|
mapName: "buisson",
|
||||||
modelPath: "/models/buisson/model.gltf",
|
modelPath: "/models/buisson/model.gltf",
|
||||||
scaleMultiplier: 2,
|
scaleMultiplier: 1.5,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
windStrength: 0.08,
|
windStrength: 0.08,
|
||||||
@@ -11,7 +11,7 @@ export const VEGETATION_TYPES = {
|
|||||||
sapin: {
|
sapin: {
|
||||||
mapName: "sapin",
|
mapName: "sapin",
|
||||||
modelPath: "/models/sapin/model.gltf",
|
modelPath: "/models/sapin/model.gltf",
|
||||||
scaleMultiplier: 5,
|
scaleMultiplier: 4,
|
||||||
castShadow: true,
|
castShadow: true,
|
||||||
receiveShadow: true,
|
receiveShadow: true,
|
||||||
windStrength: 0.04,
|
windStrength: 0.04,
|
||||||
@@ -13,12 +13,3 @@ export const WIND_BOUNDS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type WindState = typeof WIND_DEFAULTS;
|
export type WindState = typeof WIND_DEFAULTS;
|
||||||
|
|
||||||
export function getWindVector(wind: WindState): { x: number; z: number } {
|
|
||||||
const intensity = wind.speed * wind.strength;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: Math.cos(wind.direction) * intensity,
|
|
||||||
z: Math.sin(wind.direction) * intensity,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ export function useRepairMovementLocked(): boolean {
|
|||||||
switch (state.mainState) {
|
switch (state.mainState) {
|
||||||
case "ebike":
|
case "ebike":
|
||||||
return isRepairMovementLocked(state.ebike.currentStep);
|
return isRepairMovementLocked(state.ebike.currentStep);
|
||||||
case "pylone":
|
case "pylon":
|
||||||
return isRepairMovementLocked(state.pylone.currentStep);
|
return isRepairMovementLocked(state.pylon.currentStep);
|
||||||
case "ferme":
|
case "farm":
|
||||||
return isRepairMovementLocked(state.ferme.currentStep);
|
return isRepairMovementLocked(state.farm.currentStep);
|
||||||
case "intro":
|
case "intro":
|
||||||
case "outro":
|
case "outro":
|
||||||
return false;
|
return false;
|
||||||
@@ -23,6 +23,7 @@ function isRepairMovementLocked(step: MissionStep): boolean {
|
|||||||
step === "fragmented" ||
|
step === "fragmented" ||
|
||||||
step === "scanning" ||
|
step === "scanning" ||
|
||||||
step === "repairing" ||
|
step === "repairing" ||
|
||||||
step === "reassembling"
|
step === "reassembling" ||
|
||||||
|
step === "done"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
HAND_TRACKING_JPEG_QUALITY,
|
HAND_TRACKING_JPEG_QUALITY,
|
||||||
HAND_TRACKING_RESPONSE_TIMEOUT_MS,
|
HAND_TRACKING_RESPONSE_TIMEOUT_MS,
|
||||||
HAND_TRACKING_TARGET_FPS,
|
HAND_TRACKING_TARGET_FPS,
|
||||||
getHandTrackingWsUrl,
|
|
||||||
} from "@/data/handTrackingConfig";
|
} from "@/data/handTrackingConfig";
|
||||||
|
import { getHandTrackingWsUrl } from "@/utils/handTracking/handTrackingEndpoint";
|
||||||
import {
|
import {
|
||||||
INITIAL_HAND_TRACKING_SNAPSHOT,
|
INITIAL_HAND_TRACKING_SNAPSHOT,
|
||||||
getCameraStreamWithTimeout,
|
getCameraStreamWithTimeout,
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { disposeObject3D } from "@/utils/three/dispose";
|
|
||||||
|
|
||||||
export function useClonedObject<T extends THREE.Object3D>(object: T): T {
|
export function useClonedObject<T extends THREE.Object3D>(object: T): T {
|
||||||
const clone = useMemo(() => object.clone(true) as T, [object]);
|
return useMemo(() => object.clone(true) as T, [object]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
disposeObject3D(clone);
|
|
||||||
};
|
|
||||||
}, [clone]);
|
|
||||||
|
|
||||||
return clone;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,24 @@ interface TerrainHeightSampler {
|
|||||||
getHeight: (x: number, z: number) => number | null;
|
getHeight: (x: number, z: number) => number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CachedTerrainHeightSampler {
|
||||||
|
key: string;
|
||||||
|
sampler: TerrainHeightSampler;
|
||||||
|
}
|
||||||
|
|
||||||
|
const terrainSamplerCache = new WeakMap<
|
||||||
|
THREE.Object3D,
|
||||||
|
CachedTerrainHeightSampler
|
||||||
|
>();
|
||||||
|
|
||||||
|
function createTerrainSamplerCacheKey(
|
||||||
|
position: Vector3Tuple,
|
||||||
|
rotation: Vector3Tuple,
|
||||||
|
scale: Vector3Tuple,
|
||||||
|
): string {
|
||||||
|
return `${position.join(",")}|${rotation.join(",")}|${scale.join(",")}`;
|
||||||
|
}
|
||||||
|
|
||||||
function createTerrainHeightSampler(
|
function createTerrainHeightSampler(
|
||||||
scene: THREE.Object3D,
|
scene: THREE.Object3D,
|
||||||
position: Vector3Tuple,
|
position: Vector3Tuple,
|
||||||
@@ -64,10 +82,23 @@ export function useTerrainHeightSampler(): TerrainHeightSampler {
|
|||||||
const rotation = terrainNode?.rotation ?? DEFAULT_TERRAIN_ROTATION;
|
const rotation = terrainNode?.rotation ?? DEFAULT_TERRAIN_ROTATION;
|
||||||
const scale = terrainNode?.scale ?? DEFAULT_TERRAIN_SCALE;
|
const scale = terrainNode?.scale ?? DEFAULT_TERRAIN_SCALE;
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(() => {
|
||||||
() => createTerrainHeightSampler(scene, position, rotation, scale),
|
const key = createTerrainSamplerCacheKey(position, rotation, scale);
|
||||||
[position, rotation, scale, scene],
|
const cached = terrainSamplerCache.get(scene);
|
||||||
);
|
|
||||||
|
if (cached?.key === key) {
|
||||||
|
return cached.sampler;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampler = createTerrainHeightSampler(
|
||||||
|
scene,
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
terrainSamplerCache.set(scene, { key, sampler });
|
||||||
|
return sampler;
|
||||||
|
}, [position, rotation, scale, scene]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTerrainSnappedPosition(
|
export function useTerrainSnappedPosition(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { INSTANCED_MAP_EXCEPTIONS } from "@/world/vegetation/vegetationConfig";
|
import { INSTANCED_MAP_EXCEPTIONS } from "@/data/world/vegetationConfig";
|
||||||
import type { MapNode } from "@/types/map/mapScene";
|
import type { MapNode } from "@/types/map/mapScene";
|
||||||
import {
|
import {
|
||||||
type MapNodeInstanceTransform,
|
type MapNodeInstanceTransform,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
|
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ export function useVisibleWorldChunks<TChunk extends WorldChunkLike>(
|
|||||||
): readonly TChunk[] {
|
): readonly TChunk[] {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval);
|
const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval);
|
||||||
|
const activeChunkKeysRef = useRef<Set<string>>(new Set());
|
||||||
const [activeChunkKeys, setActiveChunkKeys] = useState<Set<string>>(
|
const [activeChunkKeys, setActiveChunkKeys] = useState<Set<string>>(
|
||||||
() => new Set(),
|
() => new Set(),
|
||||||
);
|
);
|
||||||
@@ -32,7 +33,7 @@ export function useVisibleWorldChunks<TChunk extends WorldChunkLike>(
|
|||||||
chunk.centerX - cameraX,
|
chunk.centerX - cameraX,
|
||||||
chunk.centerZ - cameraZ,
|
chunk.centerZ - cameraZ,
|
||||||
);
|
);
|
||||||
const wasActive = activeChunkKeys.has(chunk.key);
|
const wasActive = activeChunkKeysRef.current.has(chunk.key);
|
||||||
const radius = wasActive
|
const radius = wasActive
|
||||||
? CHUNK_CONFIG.unloadRadius
|
? CHUNK_CONFIG.unloadRadius
|
||||||
: CHUNK_CONFIG.loadRadius;
|
: CHUNK_CONFIG.loadRadius;
|
||||||
@@ -42,10 +43,11 @@ export function useVisibleWorldChunks<TChunk extends WorldChunkLike>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (areSetsEqual(nextKeys, activeChunkKeys)) return;
|
if (areSetsEqual(nextKeys, activeChunkKeysRef.current)) return;
|
||||||
|
|
||||||
|
activeChunkKeysRef.current = nextKeys;
|
||||||
setActiveChunkKeys(nextKeys);
|
setActiveChunkKeys(nextKeys);
|
||||||
}, [activeChunkKeys, camera, chunks]);
|
}, [camera, chunks]);
|
||||||
|
|
||||||
useFrame(({ clock }) => {
|
useFrame(({ clock }) => {
|
||||||
if (!streamingEnabled) return;
|
if (!streamingEnabled) return;
|
||||||
@@ -57,18 +59,26 @@ export function useVisibleWorldChunks<TChunk extends WorldChunkLike>(
|
|||||||
updateActiveChunks();
|
updateActiveChunks();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!streamingEnabled) return chunks;
|
return useMemo(() => {
|
||||||
|
if (!streamingEnabled) return chunks;
|
||||||
|
|
||||||
return chunks.filter((chunk) => {
|
return chunks.filter((chunk) => {
|
||||||
if (activeChunkKeys.size > 0) {
|
if (activeChunkKeys.size > 0) {
|
||||||
return activeChunkKeys.has(chunk.key);
|
return activeChunkKeys.has(chunk.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Math.hypot(
|
Math.hypot(
|
||||||
chunk.centerX - camera.position.x,
|
chunk.centerX - camera.position.x,
|
||||||
chunk.centerZ - camera.position.z,
|
chunk.centerZ - camera.position.z,
|
||||||
) <= CHUNK_CONFIG.loadRadius
|
) <= CHUNK_CONFIG.loadRadius
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
}, [
|
||||||
|
activeChunkKeys,
|
||||||
|
camera.position.x,
|
||||||
|
camera.position.z,
|
||||||
|
chunks,
|
||||||
|
streamingEnabled,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DEFAULT_CATEGORY_VOLUMES } from "@/data/audioConfig";
|
||||||
import { logger } from "@/utils/core/Logger";
|
import { logger } from "@/utils/core/Logger";
|
||||||
|
|
||||||
export type AudioCategory = "music" | "sfx" | "dialogue";
|
export type AudioCategory = "music" | "sfx" | "dialogue";
|
||||||
@@ -7,12 +8,6 @@ interface AudioContextWindow extends Window {
|
|||||||
webkitAudioContext?: typeof AudioContext;
|
webkitAudioContext?: typeof AudioContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CATEGORY_VOLUMES: Record<AudioCategory, number> = {
|
|
||||||
music: 1,
|
|
||||||
sfx: 1,
|
|
||||||
dialogue: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PlaySoundOptions {
|
interface PlaySoundOptions {
|
||||||
category?: OneShotAudioCategory;
|
category?: OneShotAudioCategory;
|
||||||
pan?: number;
|
pan?: number;
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import { isGameStep, isMainGameState } from "@/data/game/gameStateConfig";
|
||||||
import {
|
import {
|
||||||
isGameStep,
|
|
||||||
isMainGameState,
|
|
||||||
type GameStep,
|
|
||||||
type MainGameState,
|
|
||||||
} from "@/types/game";
|
|
||||||
import {
|
|
||||||
isRepairMissionId,
|
|
||||||
isMissionStep,
|
|
||||||
getNextMissionStep,
|
getNextMissionStep,
|
||||||
getPreviousMissionStep,
|
getPreviousMissionStep,
|
||||||
|
isMissionStep,
|
||||||
|
isRepairMissionId,
|
||||||
|
} from "@/data/gameplay/repairMissionState";
|
||||||
|
import type { GameStep, MainGameState } from "@/types/game";
|
||||||
|
import {
|
||||||
type MissionStep,
|
type MissionStep,
|
||||||
type RepairMissionId,
|
type RepairMissionId,
|
||||||
} from "@/types/gameplay/repairMission";
|
} from "@/types/gameplay/repairMission";
|
||||||
@@ -49,10 +47,10 @@ export interface GameState {
|
|||||||
ebike: MissionState & {
|
ebike: MissionState & {
|
||||||
isRepaired: boolean;
|
isRepaired: boolean;
|
||||||
};
|
};
|
||||||
pylone: MissionState & {
|
pylon: MissionState & {
|
||||||
isPowered: boolean;
|
isPowered: boolean;
|
||||||
};
|
};
|
||||||
ferme: MissionState & {
|
farm: MissionState & {
|
||||||
irrigationFixed: boolean;
|
irrigationFixed: boolean;
|
||||||
};
|
};
|
||||||
outro: {
|
outro: {
|
||||||
@@ -71,14 +69,14 @@ interface GameActions {
|
|||||||
setIntroState: (intro: Partial<IntroState>) => void;
|
setIntroState: (intro: Partial<IntroState>) => void;
|
||||||
setPlayerName: (playerName: string) => void;
|
setPlayerName: (playerName: string) => void;
|
||||||
setEbikeState: (ebike: Partial<GameState["ebike"]>) => void;
|
setEbikeState: (ebike: Partial<GameState["ebike"]>) => void;
|
||||||
setPyloneState: (pylone: Partial<GameState["pylone"]>) => void;
|
setPylonState: (pylon: Partial<GameState["pylon"]>) => void;
|
||||||
setFermeState: (ferme: Partial<GameState["ferme"]>) => void;
|
setFarmState: (farm: Partial<GameState["farm"]>) => void;
|
||||||
setOutroState: (outro: Partial<GameState["outro"]>) => void;
|
setOutroState: (outro: Partial<GameState["outro"]>) => void;
|
||||||
setMissionStep: (mission: RepairMissionId, step: MissionStep) => void;
|
setMissionStep: (mission: RepairMissionId, step: MissionStep) => void;
|
||||||
completeIntro: () => void;
|
completeIntro: () => void;
|
||||||
completeEbike: () => void;
|
completeEbike: () => void;
|
||||||
completePylone: () => void;
|
completePylon: () => void;
|
||||||
completeFerme: () => void;
|
completeFarm: () => void;
|
||||||
completeMission: (mission: RepairMissionId) => void;
|
completeMission: (mission: RepairMissionId) => void;
|
||||||
startOutro: () => void;
|
startOutro: () => void;
|
||||||
advanceGameState: () => void;
|
advanceGameState: () => void;
|
||||||
@@ -110,6 +108,10 @@ function completeIntroState(state: GameState): GameStateUpdate {
|
|||||||
hasCompleted: true,
|
hasCompleted: true,
|
||||||
isEbikeUnlocked: true,
|
isEbikeUnlocked: true,
|
||||||
},
|
},
|
||||||
|
missionFlow: {
|
||||||
|
...state.missionFlow,
|
||||||
|
canMove: true,
|
||||||
|
},
|
||||||
ebike: {
|
ebike: {
|
||||||
...state.ebike,
|
...state.ebike,
|
||||||
currentStep: "locked",
|
currentStep: "locked",
|
||||||
@@ -119,39 +121,39 @@ function completeIntroState(state: GameState): GameStateUpdate {
|
|||||||
|
|
||||||
function completeEbikeState(state: GameState): GameStateUpdate {
|
function completeEbikeState(state: GameState): GameStateUpdate {
|
||||||
return {
|
return {
|
||||||
mainState: "pylone",
|
mainState: "pylon",
|
||||||
ebike: {
|
ebike: {
|
||||||
...state.ebike,
|
...state.ebike,
|
||||||
currentStep: "done",
|
currentStep: "done",
|
||||||
isRepaired: true,
|
isRepaired: true,
|
||||||
},
|
},
|
||||||
pylone: {
|
pylon: {
|
||||||
...state.pylone,
|
...state.pylon,
|
||||||
currentStep: "waiting",
|
currentStep: "waiting",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function completePyloneState(state: GameState): GameStateUpdate {
|
function completePylonState(state: GameState): GameStateUpdate {
|
||||||
return {
|
return {
|
||||||
mainState: "ferme",
|
mainState: "farm",
|
||||||
pylone: {
|
pylon: {
|
||||||
...state.pylone,
|
...state.pylon,
|
||||||
currentStep: "done",
|
currentStep: "done",
|
||||||
isPowered: true,
|
isPowered: true,
|
||||||
},
|
},
|
||||||
ferme: {
|
farm: {
|
||||||
...state.ferme,
|
...state.farm,
|
||||||
currentStep: "waiting",
|
currentStep: "waiting",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function completeFermeState(state: GameState): GameStateUpdate {
|
function completeFarmState(state: GameState): GameStateUpdate {
|
||||||
return {
|
return {
|
||||||
mainState: "outro",
|
mainState: "outro",
|
||||||
ferme: {
|
farm: {
|
||||||
...state.ferme,
|
...state.farm,
|
||||||
currentStep: "done",
|
currentStep: "done",
|
||||||
irrigationFixed: true,
|
irrigationFixed: true,
|
||||||
},
|
},
|
||||||
@@ -182,10 +184,10 @@ function completeMissionState(
|
|||||||
switch (mission) {
|
switch (mission) {
|
||||||
case "ebike":
|
case "ebike":
|
||||||
return completeEbikeState(state);
|
return completeEbikeState(state);
|
||||||
case "pylone":
|
case "pylon":
|
||||||
return completePyloneState(state);
|
return completePylonState(state);
|
||||||
case "ferme":
|
case "farm":
|
||||||
return completeFermeState(state);
|
return completeFarmState(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,12 +245,12 @@ function createInitialGameState(): GameState {
|
|||||||
dialogueAudio: null,
|
dialogueAudio: null,
|
||||||
isRepaired: false,
|
isRepaired: false,
|
||||||
},
|
},
|
||||||
pylone: {
|
pylon: {
|
||||||
currentStep: "locked",
|
currentStep: "locked",
|
||||||
dialogueAudio: null,
|
dialogueAudio: null,
|
||||||
isPowered: false,
|
isPowered: false,
|
||||||
},
|
},
|
||||||
ferme: {
|
farm: {
|
||||||
currentStep: "locked",
|
currentStep: "locked",
|
||||||
dialogueAudio: null,
|
dialogueAudio: null,
|
||||||
irrigationFixed: false,
|
irrigationFixed: false,
|
||||||
@@ -321,8 +323,8 @@ function hydrateDebugGameState(initial: GameState, value: unknown): GameState {
|
|||||||
if (!isRecord(value)) return initial;
|
if (!isRecord(value)) return initial;
|
||||||
|
|
||||||
const ebike = hydrateMissionState(initial.ebike, value.ebike);
|
const ebike = hydrateMissionState(initial.ebike, value.ebike);
|
||||||
const pylone = hydrateMissionState(initial.pylone, value.pylone);
|
const pylon = hydrateMissionState(initial.pylon, value.pylon);
|
||||||
const ferme = hydrateMissionState(initial.ferme, value.ferme);
|
const farm = hydrateMissionState(initial.farm, value.farm);
|
||||||
const outro = isRecord(value.outro) ? value.outro : null;
|
const outro = isRecord(value.outro) ? value.outro : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -344,19 +346,19 @@ function hydrateDebugGameState(initial: GameState, value: unknown): GameState {
|
|||||||
? value.ebike.isRepaired
|
? value.ebike.isRepaired
|
||||||
: initial.ebike.isRepaired,
|
: initial.ebike.isRepaired,
|
||||||
},
|
},
|
||||||
pylone: {
|
pylon: {
|
||||||
...pylone,
|
...pylon,
|
||||||
isPowered:
|
isPowered:
|
||||||
isRecord(value.pylone) && isBoolean(value.pylone.isPowered)
|
isRecord(value.pylon) && isBoolean(value.pylon.isPowered)
|
||||||
? value.pylone.isPowered
|
? value.pylon.isPowered
|
||||||
: initial.pylone.isPowered,
|
: initial.pylon.isPowered,
|
||||||
},
|
},
|
||||||
ferme: {
|
farm: {
|
||||||
...ferme,
|
...farm,
|
||||||
irrigationFixed:
|
irrigationFixed:
|
||||||
isRecord(value.ferme) && isBoolean(value.ferme.irrigationFixed)
|
isRecord(value.farm) && isBoolean(value.farm.irrigationFixed)
|
||||||
? value.ferme.irrigationFixed
|
? value.farm.irrigationFixed
|
||||||
: initial.ferme.irrigationFixed,
|
: initial.farm.irrigationFixed,
|
||||||
},
|
},
|
||||||
outro: {
|
outro: {
|
||||||
dialogueAudio:
|
dialogueAudio:
|
||||||
@@ -385,8 +387,8 @@ function pickGameState(state: GameStore): GameState {
|
|||||||
missionFlow: state.missionFlow,
|
missionFlow: state.missionFlow,
|
||||||
intro: state.intro,
|
intro: state.intro,
|
||||||
ebike: state.ebike,
|
ebike: state.ebike,
|
||||||
pylone: state.pylone,
|
pylon: state.pylon,
|
||||||
ferme: state.ferme,
|
farm: state.farm,
|
||||||
outro: state.outro,
|
outro: state.outro,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -417,18 +419,18 @@ export const useGameStore = create<GameStore>()((set) => ({
|
|||||||
})),
|
})),
|
||||||
setEbikeState: (ebike) =>
|
setEbikeState: (ebike) =>
|
||||||
set((state) => ({ ebike: { ...state.ebike, ...ebike } })),
|
set((state) => ({ ebike: { ...state.ebike, ...ebike } })),
|
||||||
setPyloneState: (pylone) =>
|
setPylonState: (pylon) =>
|
||||||
set((state) => ({ pylone: { ...state.pylone, ...pylone } })),
|
set((state) => ({ pylon: { ...state.pylon, ...pylon } })),
|
||||||
setFermeState: (ferme) =>
|
setFarmState: (farm) =>
|
||||||
set((state) => ({ ferme: { ...state.ferme, ...ferme } })),
|
set((state) => ({ farm: { ...state.farm, ...farm } })),
|
||||||
setOutroState: (outro) =>
|
setOutroState: (outro) =>
|
||||||
set((state) => ({ outro: { ...state.outro, ...outro } })),
|
set((state) => ({ outro: { ...state.outro, ...outro } })),
|
||||||
setMissionStep: (mission, step) =>
|
setMissionStep: (mission, step) =>
|
||||||
set((state) => setMissionStepState(state, mission, step)),
|
set((state) => setMissionStepState(state, mission, step)),
|
||||||
completeIntro: () => set(completeIntroState),
|
completeIntro: () => set(completeIntroState),
|
||||||
completeEbike: () => set((state) => completeMissionState(state, "ebike")),
|
completeEbike: () => set((state) => completeMissionState(state, "ebike")),
|
||||||
completePylone: () => set((state) => completeMissionState(state, "pylone")),
|
completePylon: () => set((state) => completeMissionState(state, "pylon")),
|
||||||
completeFerme: () => set((state) => completeMissionState(state, "ferme")),
|
completeFarm: () => set((state) => completeMissionState(state, "farm")),
|
||||||
completeMission: (mission) =>
|
completeMission: (mission) =>
|
||||||
set((state) => completeMissionState(state, mission)),
|
set((state) => completeMissionState(state, mission)),
|
||||||
startOutro: () => set(startOutroState),
|
startOutro: () => set(startOutroState),
|
||||||
|
|||||||
@@ -1,38 +1,18 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import {
|
||||||
|
MAP_PERFORMANCE_GROUP_NAMES,
|
||||||
|
MAP_PERFORMANCE_MODEL_GROUPS,
|
||||||
|
MAP_PERFORMANCE_MODEL_NAMES,
|
||||||
|
type MapPerformanceGroupName,
|
||||||
|
type MapPerformanceModelName,
|
||||||
|
} from "@/data/world/mapPerformanceConfig";
|
||||||
|
|
||||||
export type MapPerformanceGroupName =
|
export {
|
||||||
| "vegetation"
|
MAP_PERFORMANCE_GROUP_NAMES,
|
||||||
| "crops"
|
MAP_PERFORMANCE_MODEL_NAMES,
|
||||||
| "trees"
|
type MapPerformanceGroupName,
|
||||||
| "buildings"
|
type MapPerformanceModelName,
|
||||||
| "landmarks"
|
};
|
||||||
| "props"
|
|
||||||
| "terrain"
|
|
||||||
| "sky";
|
|
||||||
|
|
||||||
export type MapPerformanceModelName =
|
|
||||||
| "buisson"
|
|
||||||
| "arbre"
|
|
||||||
| "sapin"
|
|
||||||
| "champdeble"
|
|
||||||
| "champdesoja"
|
|
||||||
| "champsdetournesol"
|
|
||||||
| "ecole"
|
|
||||||
| "generateur"
|
|
||||||
| "fermeverticale"
|
|
||||||
| "lafabrik"
|
|
||||||
| "immeuble1"
|
|
||||||
| "eolienne"
|
|
||||||
| "pylone"
|
|
||||||
| "boiteauxlettres"
|
|
||||||
| "maison1"
|
|
||||||
| "panneauaffichage"
|
|
||||||
| "panneauclassique"
|
|
||||||
| "panneaufleche"
|
|
||||||
| "panneausolaire"
|
|
||||||
| "parcebike"
|
|
||||||
| "terrain"
|
|
||||||
| "sky";
|
|
||||||
|
|
||||||
export interface MapPerformanceVisibility {
|
export interface MapPerformanceVisibility {
|
||||||
groups: Record<MapPerformanceGroupName, boolean>;
|
groups: Record<MapPerformanceGroupName, boolean>;
|
||||||
@@ -47,70 +27,6 @@ interface MapPerformanceActions {
|
|||||||
|
|
||||||
type MapPerformanceStore = MapPerformanceVisibility & MapPerformanceActions;
|
type MapPerformanceStore = MapPerformanceVisibility & MapPerformanceActions;
|
||||||
|
|
||||||
export const MAP_PERFORMANCE_GROUP_NAMES: readonly MapPerformanceGroupName[] = [
|
|
||||||
"vegetation",
|
|
||||||
"crops",
|
|
||||||
"trees",
|
|
||||||
"buildings",
|
|
||||||
"landmarks",
|
|
||||||
"props",
|
|
||||||
"terrain",
|
|
||||||
"sky",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const MAP_PERFORMANCE_MODEL_NAMES: readonly MapPerformanceModelName[] = [
|
|
||||||
"buisson",
|
|
||||||
"arbre",
|
|
||||||
"sapin",
|
|
||||||
"champdeble",
|
|
||||||
"champdesoja",
|
|
||||||
"champsdetournesol",
|
|
||||||
"ecole",
|
|
||||||
"generateur",
|
|
||||||
"fermeverticale",
|
|
||||||
"lafabrik",
|
|
||||||
"immeuble1",
|
|
||||||
"eolienne",
|
|
||||||
"pylone",
|
|
||||||
"boiteauxlettres",
|
|
||||||
"maison1",
|
|
||||||
"panneauaffichage",
|
|
||||||
"panneauclassique",
|
|
||||||
"panneaufleche",
|
|
||||||
"panneausolaire",
|
|
||||||
"parcebike",
|
|
||||||
"terrain",
|
|
||||||
"sky",
|
|
||||||
];
|
|
||||||
|
|
||||||
const MODEL_GROUPS: Record<
|
|
||||||
MapPerformanceModelName,
|
|
||||||
readonly MapPerformanceGroupName[]
|
|
||||||
> = {
|
|
||||||
buisson: ["vegetation"],
|
|
||||||
arbre: ["vegetation", "trees"],
|
|
||||||
sapin: ["vegetation", "trees"],
|
|
||||||
champdeble: ["vegetation", "crops"],
|
|
||||||
champdesoja: ["vegetation", "crops"],
|
|
||||||
champsdetournesol: ["vegetation", "crops"],
|
|
||||||
ecole: ["buildings", "landmarks"],
|
|
||||||
generateur: ["landmarks"],
|
|
||||||
fermeverticale: ["buildings", "landmarks"],
|
|
||||||
lafabrik: ["buildings", "landmarks"],
|
|
||||||
immeuble1: ["buildings"],
|
|
||||||
eolienne: ["props"],
|
|
||||||
pylone: ["props"],
|
|
||||||
boiteauxlettres: ["props"],
|
|
||||||
maison1: ["buildings"],
|
|
||||||
panneauaffichage: ["props"],
|
|
||||||
panneauclassique: ["props"],
|
|
||||||
panneaufleche: ["props"],
|
|
||||||
panneausolaire: ["props"],
|
|
||||||
parcebike: ["props"],
|
|
||||||
terrain: ["terrain"],
|
|
||||||
sky: ["sky"],
|
|
||||||
};
|
|
||||||
|
|
||||||
function createVisibleRecord<T extends string>(
|
function createVisibleRecord<T extends string>(
|
||||||
keys: readonly T[],
|
keys: readonly T[],
|
||||||
): Record<T, boolean> {
|
): Record<T, boolean> {
|
||||||
@@ -140,7 +56,9 @@ export function isMapModelVisible(
|
|||||||
if (!isMapPerformanceModelName(name)) return true;
|
if (!isMapPerformanceModelName(name)) return true;
|
||||||
if (!visibility.models[name]) return false;
|
if (!visibility.models[name]) return false;
|
||||||
|
|
||||||
return MODEL_GROUPS[name].every((group) => visibility.groups[group]);
|
return MAP_PERFORMANCE_MODEL_GROUPS[name].every(
|
||||||
|
(group) => visibility.groups[group],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMapPerformanceStore = create<MapPerformanceStore>()((set) => ({
|
export const useMapPerformanceStore = create<MapPerformanceStore>()((set) => ({
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { EditorScene } from "@/components/editor/scene/EditorScene";
|
|||||||
import type { EditorCinematicPreviewRequest } from "@/components/editor/scene/EditorScene";
|
import type { EditorCinematicPreviewRequest } from "@/components/editor/scene/EditorScene";
|
||||||
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
|
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
|
||||||
import { Subtitles } from "@/components/ui/Subtitles";
|
import { Subtitles } from "@/components/ui/Subtitles";
|
||||||
|
import { INITIAL_SCENE_LOADING_STATE } from "@/data/world/sceneLoadingConfig";
|
||||||
import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
|
import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
|
||||||
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
|
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
|
||||||
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData";
|
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData";
|
||||||
@@ -16,7 +17,6 @@ import type {
|
|||||||
TransformMode,
|
TransformMode,
|
||||||
} from "@/types/editor/editor";
|
} from "@/types/editor/editor";
|
||||||
import {
|
import {
|
||||||
INITIAL_SCENE_LOADING_STATE,
|
|
||||||
type SceneLoadingChangeHandler,
|
type SceneLoadingChangeHandler,
|
||||||
type SceneLoadingState,
|
type SceneLoadingState,
|
||||||
} from "@/types/world/sceneLoading";
|
} from "@/types/world/sceneLoading";
|
||||||
|
|||||||
+2
-4
@@ -6,12 +6,10 @@ 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 { BienvenueDisplay, IntroUI } from "@/components/ui/IntroUI";
|
||||||
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
|
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
|
||||||
|
import { INITIAL_SCENE_LOADING_STATE } from "@/data/world/sceneLoadingConfig";
|
||||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
|
import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
|
||||||
import {
|
import type { SceneLoadingState } from "@/types/world/sceneLoading";
|
||||||
INITIAL_SCENE_LOADING_STATE,
|
|
||||||
type SceneLoadingState,
|
|
||||||
} from "@/types/world/sceneLoading";
|
|
||||||
import { logger } from "@/utils/core/Logger";
|
import { logger } from "@/utils/core/Logger";
|
||||||
import { World } from "@/world/World";
|
import { World } from "@/world/World";
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ export function HandTrackingProvider({
|
|||||||
switch (state.mainState) {
|
switch (state.mainState) {
|
||||||
case "ebike":
|
case "ebike":
|
||||||
return REPAIR_HAND_TRACKING_STEPS.has(state.ebike.currentStep);
|
return REPAIR_HAND_TRACKING_STEPS.has(state.ebike.currentStep);
|
||||||
case "pylone":
|
case "pylon":
|
||||||
return REPAIR_HAND_TRACKING_STEPS.has(state.pylone.currentStep);
|
return REPAIR_HAND_TRACKING_STEPS.has(state.pylon.currentStep);
|
||||||
case "ferme":
|
case "farm":
|
||||||
return REPAIR_HAND_TRACKING_STEPS.has(state.ferme.currentStep);
|
return REPAIR_HAND_TRACKING_STEPS.has(state.farm.currentStep);
|
||||||
case "intro":
|
case "intro":
|
||||||
case "outro":
|
case "outro":
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+1
-34
@@ -12,40 +12,7 @@ export type GameStep =
|
|||||||
| "manipulation"
|
| "manipulation"
|
||||||
| "outOfFabrik";
|
| "outOfFabrik";
|
||||||
|
|
||||||
export const GAME_STEPS: readonly GameStep[] = [
|
export type MainGameState = "intro" | "ebike" | "pylon" | "farm" | "outro";
|
||||||
"intro",
|
|
||||||
"start-intro",
|
|
||||||
"naming",
|
|
||||||
"bienvenue",
|
|
||||||
"star-move",
|
|
||||||
"mission2",
|
|
||||||
"searching",
|
|
||||||
"helped",
|
|
||||||
"manipulation",
|
|
||||||
"outOfFabrik",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const GAME_STEP_VALUES: ReadonlySet<string> = new Set(GAME_STEPS);
|
|
||||||
|
|
||||||
export type MainGameState = "intro" | "ebike" | "pylone" | "ferme" | "outro";
|
|
||||||
|
|
||||||
export const MAIN_GAME_STATES: readonly MainGameState[] = [
|
|
||||||
"intro",
|
|
||||||
"ebike",
|
|
||||||
"pylone",
|
|
||||||
"ferme",
|
|
||||||
"outro",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const MAIN_GAME_STATE_VALUES: ReadonlySet<string> = new Set(MAIN_GAME_STATES);
|
|
||||||
|
|
||||||
export function isGameStep(value: unknown): value is GameStep {
|
|
||||||
return typeof value === "string" && GAME_STEP_VALUES.has(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMainGameState(value: unknown): value is MainGameState {
|
|
||||||
return typeof value === "string" && MAIN_GAME_STATE_VALUES.has(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Zone {
|
export interface Zone {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type {
|
|||||||
Vector3Tuple,
|
Vector3Tuple,
|
||||||
} from "@/types/three/three";
|
} from "@/types/three/three";
|
||||||
|
|
||||||
export type RepairMissionId = "ebike" | "pylone" | "ferme";
|
export type RepairMissionId = "ebike" | "pylon" | "farm";
|
||||||
|
|
||||||
export interface RepairMissionCaseConfig {
|
export interface RepairMissionCaseConfig {
|
||||||
position: Vector3Tuple;
|
position: Vector3Tuple;
|
||||||
@@ -53,68 +53,3 @@ export type MissionStep =
|
|||||||
| "repairing"
|
| "repairing"
|
||||||
| "reassembling"
|
| "reassembling"
|
||||||
| "done";
|
| "done";
|
||||||
|
|
||||||
const REPAIR_MISSION_IDS = ["ebike", "pylone", "ferme"] as const;
|
|
||||||
const REPAIR_MISSION_ID_VALUES: ReadonlySet<string> = new Set(
|
|
||||||
REPAIR_MISSION_IDS,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const MISSION_STEPS = [
|
|
||||||
"locked",
|
|
||||||
"waiting",
|
|
||||||
"inspected",
|
|
||||||
"fragmented",
|
|
||||||
"scanning",
|
|
||||||
"repairing",
|
|
||||||
"reassembling",
|
|
||||||
"done",
|
|
||||||
] as const satisfies readonly MissionStep[];
|
|
||||||
const MISSION_STEP_VALUES: ReadonlySet<string> = new Set(MISSION_STEPS);
|
|
||||||
|
|
||||||
export function isRepairMissionId(value: string): value is RepairMissionId {
|
|
||||||
return REPAIR_MISSION_ID_VALUES.has(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMissionStep(value: string): value is MissionStep {
|
|
||||||
return MISSION_STEP_VALUES.has(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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,9 +7,3 @@ export interface SceneLoadingState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SceneLoadingChangeHandler = (state: SceneLoadingState) => void;
|
export type SceneLoadingChangeHandler = (state: SceneLoadingState) => void;
|
||||||
|
|
||||||
export const INITIAL_SCENE_LOADING_STATE: SceneLoadingState = {
|
|
||||||
currentStep: "Initialisation du jeu",
|
|
||||||
progress: 0,
|
|
||||||
status: "loading",
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
const HAND_TRACKING_LOCAL_WS_URL = "ws://localhost:8000/ws";
|
||||||
|
const HAND_TRACKING_PROD_WS_URL = "wss://handtracking.la-fabrik.fr/ws";
|
||||||
|
|
||||||
|
export function getHandTrackingWsUrl(): string {
|
||||||
|
const configuredUrl = import.meta.env.VITE_HAND_TRACKING_WS_URL;
|
||||||
|
|
||||||
|
if (configuredUrl) {
|
||||||
|
return configuredUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return import.meta.env.DEV
|
||||||
|
? HAND_TRACKING_LOCAL_WS_URL
|
||||||
|
: HAND_TRACKING_PROD_WS_URL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { MAP_INSTANCING_ASSETS } from "@/data/world/mapInstancingConfig";
|
||||||
|
|
||||||
|
const MAP_INSTANCED_NODE_NAMES: ReadonlySet<string> = new Set(
|
||||||
|
Object.values(MAP_INSTANCING_ASSETS)
|
||||||
|
.filter((config) => config.enabled)
|
||||||
|
.map((config) => config.mapName),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function isInstancedMapNodeName(name: string): boolean {
|
||||||
|
return MAP_INSTANCED_NODE_NAMES.has(name);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { MapNode } from "@/types/map/mapScene";
|
import type { MapNode } from "@/types/map/mapScene";
|
||||||
import { isInstancedMapNodeName } from "@/data/world/mapInstancingConfig";
|
import { isInstancedMapNodeName } from "@/utils/map/isInstancedMapNodeName";
|
||||||
|
|
||||||
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking", "terrain"]);
|
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking", "terrain"]);
|
||||||
const RUNTIME_VEGETATION_NODE_NAMES = new Set([
|
const RUNTIME_VEGETATION_NODE_NAMES = new Set([
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { WindState } from "@/data/world/windConfig";
|
||||||
|
|
||||||
|
export function getWindVector(wind: WindState): { x: number; z: number } {
|
||||||
|
const intensity = wind.speed * wind.strength;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: Math.cos(wind.direction) * intensity,
|
||||||
|
z: Math.sin(wind.direction) * intensity,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
import { GameMapCollision } from "@/world/GameMapCollision";
|
import { GameMapCollision } from "@/world/GameMapCollision";
|
||||||
import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance";
|
import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance";
|
||||||
import { isGeneratedMapModelName } from "@/world/map-generated/generatedMapModelConfig";
|
import { isGeneratedMapModelName } from "@/data/world/generatedMapModelConfig";
|
||||||
import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem";
|
import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem";
|
||||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||||
import { logger } from "@/utils/core/Logger";
|
import { logger } from "@/utils/core/Logger";
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
||||||
import { RepairGame } from "@/components/three/gameplay/RepairGame";
|
import { RepairGame } from "@/components/three/gameplay/RepairGame";
|
||||||
import {
|
import {
|
||||||
EBIKE_REPAIR_POSITION,
|
|
||||||
REPAIR_MISSION_POSITION_ENTRIES,
|
REPAIR_MISSION_POSITION_ENTRIES,
|
||||||
|
REPAIR_MISSION_TRIGGERS,
|
||||||
|
type RepairMissionTriggerConfig,
|
||||||
} from "@/data/gameplay/repairMissionAnchors";
|
} from "@/data/gameplay/repairMissionAnchors";
|
||||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
@@ -32,21 +33,31 @@ function StageAnchor({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EbikeMissionTrigger(): React.JSX.Element | null {
|
function RepairMissionTrigger({
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
config: RepairMissionTriggerConfig;
|
||||||
|
}): React.JSX.Element | null {
|
||||||
const mainState = useGameStore((state) => state.mainState);
|
const mainState = useGameStore((state) => state.mainState);
|
||||||
const ebikeStep = useGameStore((state) => state.ebike.currentStep);
|
const missionStep = useGameStore(
|
||||||
|
(state) => state[config.mission].currentStep,
|
||||||
|
);
|
||||||
const setMissionStep = useGameStore((state) => state.setMissionStep);
|
const setMissionStep = useGameStore((state) => state.setMissionStep);
|
||||||
|
const position = REPAIR_MISSION_POSITION_ENTRIES.find(
|
||||||
|
(entry) => entry.mission === config.mission,
|
||||||
|
)?.position;
|
||||||
|
|
||||||
if (mainState !== "ebike" || ebikeStep !== "locked") return null;
|
if (!position) return null;
|
||||||
|
if (mainState !== config.mission || missionStep !== "locked") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={EBIKE_REPAIR_POSITION}>
|
<group position={position}>
|
||||||
<InteractableObject
|
<InteractableObject
|
||||||
kind="trigger"
|
kind="trigger"
|
||||||
label="Réparer l'e-bike"
|
label={config.label}
|
||||||
position={EBIKE_REPAIR_POSITION}
|
position={position}
|
||||||
radius={4}
|
radius={config.radius}
|
||||||
onPress={() => setMissionStep("ebike", "waiting")}
|
onPress={() => setMissionStep(config.mission, "waiting")}
|
||||||
>
|
>
|
||||||
<mesh>
|
<mesh>
|
||||||
<sphereGeometry args={[1.3, 16, 16]} />
|
<sphereGeometry args={[1.3, 16, 16]} />
|
||||||
@@ -68,7 +79,9 @@ export function GameStageContent(): React.JSX.Element {
|
|||||||
{REPAIR_MISSION_POSITION_ENTRIES.map(({ mission, position }) => (
|
{REPAIR_MISSION_POSITION_ENTRIES.map(({ mission, position }) => (
|
||||||
<RepairGame key={mission} mission={mission} position={position} />
|
<RepairGame key={mission} mission={mission} position={position} />
|
||||||
))}
|
))}
|
||||||
<EbikeMissionTrigger />
|
{REPAIR_MISSION_TRIGGERS.map((config) => (
|
||||||
|
<RepairMissionTrigger key={config.mission} config={config} />
|
||||||
|
))}
|
||||||
{mainState === "outro" ? (
|
{mainState === "outro" ? (
|
||||||
<StageAnchor color="#fb7185" position={[0, 6, 10]} scale={1.25} />
|
<StageAnchor color="#fb7185" position={[0, 6, 10]} scale={1.25} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Suspense, useMemo, useRef } from "react";
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useFrame } from "@react-three/fiber";
|
import { useFrame } from "@react-three/fiber";
|
||||||
import { CLOUD_CONFIG } from "@/data/world/cloudConfig";
|
import { CLOUD_CONFIG } from "@/data/world/cloudConfig";
|
||||||
import { getWindVector } from "@/data/world/windConfig";
|
import { getWindVector } from "@/utils/world/windVector";
|
||||||
import { useDynamicClouds } from "@/hooks/world/useGraphicsSettings";
|
import { useDynamicClouds } from "@/hooks/world/useGraphicsSettings";
|
||||||
import { useCloudSettings } from "@/hooks/world/useCloudSettings";
|
import { useCloudSettings } from "@/hooks/world/useCloudSettings";
|
||||||
import { useWind } from "@/hooks/world/useWind";
|
import { useWind } from "@/hooks/world/useWind";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
GRASS_BASE_COLOR,
|
GRASS_BASE_COLOR,
|
||||||
GRASS_COLORS,
|
GRASS_COLORS,
|
||||||
GRASS_CONFIG,
|
GRASS_CONFIG,
|
||||||
} from "@/world/grass/grassConfig";
|
} from "@/data/world/grassConfig";
|
||||||
import {
|
import {
|
||||||
grassFragmentShader,
|
grassFragmentShader,
|
||||||
grassVertexShader,
|
grassVertexShader,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
useDynamicGrass,
|
useDynamicGrass,
|
||||||
useGrassDensity,
|
useGrassDensity,
|
||||||
} from "@/hooks/world/useGraphicsSettings";
|
} from "@/hooks/world/useGraphicsSettings";
|
||||||
import { GRASS_CONFIG } from "@/world/grass/grassConfig";
|
import { GRASS_CONFIG } from "@/data/world/grassConfig";
|
||||||
import { GrassPatch } from "@/world/grass/GrassPatch";
|
import { GrassPatch } from "@/world/grass/GrassPatch";
|
||||||
import { useTerrainGrassSampler } from "@/world/grass/useTerrainGrassSampler";
|
import { useTerrainGrassSampler } from "@/world/grass/useTerrainGrassSampler";
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { TerrainSurfaceBounds } from "@/types/world/terrainSurface";
|
|||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
import { logger } from "@/utils/core/Logger";
|
import { logger } from "@/utils/core/Logger";
|
||||||
import { getMapNodesByName } from "@/utils/map/loadMapSceneData";
|
import { getMapNodesByName } from "@/utils/map/loadMapSceneData";
|
||||||
import { GRASS_CONFIG } from "@/world/grass/grassConfig";
|
import { GRASS_CONFIG } from "@/data/world/grassConfig";
|
||||||
|
|
||||||
const RAYCAST_Y = 500;
|
const RAYCAST_Y = 500;
|
||||||
const RAYCAST_FAR = 1000;
|
const RAYCAST_FAR = 1000;
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ interface MeshMergeGroup {
|
|||||||
material: THREE.Material | THREE.Material[];
|
material: THREE.Material | THREE.Material[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const meshDataCache = new Map<string, MeshData[]>();
|
||||||
|
|
||||||
function cloneMaterial(
|
function cloneMaterial(
|
||||||
material: THREE.Material | THREE.Material[],
|
material: THREE.Material | THREE.Material[],
|
||||||
): THREE.Material | THREE.Material[] {
|
): THREE.Material | THREE.Material[] {
|
||||||
@@ -49,8 +51,6 @@ function disposeMaterialOnly(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function disposeInstancedMapMesh(mesh: THREE.InstancedMesh): void {
|
function disposeInstancedMapMesh(mesh: THREE.InstancedMesh): void {
|
||||||
mesh.geometry.dispose();
|
|
||||||
disposeMaterialOnly(mesh.material);
|
|
||||||
mesh.dispose();
|
mesh.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +183,15 @@ export function InstancedMapAsset({
|
|||||||
state.gl.capabilities.getMaxAnisotropy(),
|
state.gl.capabilities.getMaxAnisotropy(),
|
||||||
);
|
);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
const meshDataList = useMemo(() => {
|
||||||
|
const cached = meshDataCache.get(modelPath);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||||
|
const extracted = extractMeshes(scene);
|
||||||
|
meshDataCache.set(modelPath, extracted);
|
||||||
|
return extracted;
|
||||||
|
}, [maxAnisotropy, modelPath, scene]);
|
||||||
const groundedInstances = useMemo(
|
const groundedInstances = useMemo(
|
||||||
() =>
|
() =>
|
||||||
instances.map((instance) => {
|
instances.map((instance) => {
|
||||||
@@ -202,8 +211,6 @@ export function InstancedMapAsset({
|
|||||||
const group = groupRef.current;
|
const group = groupRef.current;
|
||||||
if (!group || groundedInstances.length === 0) return;
|
if (!group || groundedInstances.length === 0) return;
|
||||||
|
|
||||||
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
|
||||||
const meshDataList = extractMeshes(scene);
|
|
||||||
const geometryBottomY = getMeshBottomY(meshDataList);
|
const geometryBottomY = getMeshBottomY(meshDataList);
|
||||||
const instancedMeshes = meshDataList.map((meshData, index) => {
|
const instancedMeshes = meshDataList.map((meshData, index) => {
|
||||||
const instancedMesh = new THREE.InstancedMesh(
|
const instancedMesh = new THREE.InstancedMesh(
|
||||||
@@ -232,7 +239,7 @@ export function InstancedMapAsset({
|
|||||||
disposeInstancedMapMesh(mesh);
|
disposeInstancedMapMesh(mesh);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [castShadow, groundedInstances, maxAnisotropy, receiveShadow, scene]);
|
}, [castShadow, groundedInstances, meshDataList, receiveShadow]);
|
||||||
|
|
||||||
if (instances.length === 0) {
|
if (instances.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
export const MAP_INSTANCING_ASSETS = {
|
|
||||||
boiteauxlettres: {
|
|
||||||
mapName: "boiteauxlettres",
|
|
||||||
modelPath: "/models/boiteauxlettres/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
pylone: {
|
|
||||||
mapName: "pylone",
|
|
||||||
modelPath: "/models/pylone/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
immeuble1: {
|
|
||||||
mapName: "immeuble1",
|
|
||||||
modelPath: "/models/immeuble1/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
maison1: {
|
|
||||||
mapName: "maison1",
|
|
||||||
modelPath: "/models/maison1/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
eolienne: {
|
|
||||||
mapName: "eolienne",
|
|
||||||
modelPath: "/models/eolienne/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
parcebike: {
|
|
||||||
mapName: "parcebike",
|
|
||||||
modelPath: "/models/parcebike/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
panneauaffichage: {
|
|
||||||
mapName: "panneauaffichage",
|
|
||||||
modelPath: "/models/panneauaffichage/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
panneauclassique: {
|
|
||||||
mapName: "panneauclassique",
|
|
||||||
modelPath: "/models/panneauclassique/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
panneaufleche: {
|
|
||||||
mapName: "panneaufleche",
|
|
||||||
modelPath: "/models/panneaufleche/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
panneausolaire: {
|
|
||||||
mapName: "panneausolaire",
|
|
||||||
modelPath: "/models/panneausolaire/model.gltf",
|
|
||||||
castShadow: true,
|
|
||||||
receiveShadow: true,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type MapInstancingAssetType = keyof typeof MAP_INSTANCING_ASSETS;
|
|
||||||
|
|
||||||
export type MapInstancingAssetConfig =
|
|
||||||
(typeof MAP_INSTANCING_ASSETS)[MapInstancingAssetType];
|
|
||||||
|
|
||||||
export const MAP_INSTANCED_NODE_NAMES: ReadonlySet<string> = new Set(
|
|
||||||
Object.values(MAP_INSTANCING_ASSETS)
|
|
||||||
.filter((config) => config.enabled)
|
|
||||||
.map((config) => config.mapName),
|
|
||||||
);
|
|
||||||
|
|
||||||
export function isInstancedMapNodeName(name: string): boolean {
|
|
||||||
return MAP_INSTANCED_NODE_NAMES.has(name);
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import type { MapNode } from "@/types/editor/editor";
|
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
|
||||||
import { getMapNodes, loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
|
||||||
import {
|
|
||||||
MAP_INSTANCING_ASSETS,
|
|
||||||
type MapInstancingAssetType,
|
|
||||||
} from "@/world/map-instancing/mapInstancingConfig";
|
|
||||||
|
|
||||||
export interface MapAssetInstance {
|
|
||||||
position: Vector3Tuple;
|
|
||||||
rotation: Vector3Tuple;
|
|
||||||
scale: Vector3Tuple;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MapInstancingData = Map<MapInstancingAssetType, MapAssetInstance[]>;
|
|
||||||
|
|
||||||
function mapNodeToInstance(node: MapNode): MapAssetInstance {
|
|
||||||
return {
|
|
||||||
position: node.position,
|
|
||||||
rotation: node.rotation,
|
|
||||||
scale: node.scale,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractMapInstancingData(mapNodes: MapNode[]): MapInstancingData {
|
|
||||||
const data: MapInstancingData = new Map();
|
|
||||||
|
|
||||||
for (const [type, config] of Object.entries(MAP_INSTANCING_ASSETS)) {
|
|
||||||
if (!config.enabled) continue;
|
|
||||||
|
|
||||||
const instances = mapNodes
|
|
||||||
.filter(
|
|
||||||
(node) => node.name === config.mapName && node.type === "Object3D",
|
|
||||||
)
|
|
||||||
.map(mapNodeToInstance);
|
|
||||||
|
|
||||||
if (instances.length > 0) {
|
|
||||||
data.set(type as MapInstancingAssetType, instances);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMapInstancingData(): {
|
|
||||||
data: MapInstancingData | null;
|
|
||||||
isLoading: boolean;
|
|
||||||
} {
|
|
||||||
const [data, setData] = useState<MapInstancingData | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
const cachedNodes = getMapNodes();
|
|
||||||
|
|
||||||
if (cachedNodes) {
|
|
||||||
if (!cancelled) {
|
|
||||||
setData(extractMapInstancingData(cachedNodes));
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadMapSceneData();
|
|
||||||
const nodes = getMapNodes();
|
|
||||||
|
|
||||||
if (!cancelled && nodes) {
|
|
||||||
setData(extractMapInstancingData(nodes));
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
load();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { data, isLoading };
|
|
||||||
}
|
|
||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
PATH_DEBUG_PREVIEW_ENABLED,
|
PATH_DEBUG_PREVIEW_ENABLED,
|
||||||
PATH_TILE_RENDER_ENABLED,
|
PATH_TILE_RENDER_ENABLED,
|
||||||
PATH_TILE_MODEL_PATH,
|
PATH_TILE_MODEL_PATH,
|
||||||
} from "@/world/paths/pathConfig";
|
} from "@/data/world/pathConfig";
|
||||||
import { usePathTileData } from "@/world/paths/usePathTileData";
|
import { usePathTileData } from "@/world/paths/usePathTileData";
|
||||||
import type { MapAssetInstance } from "@/world/map-instancing/useMapInstancingData";
|
import type { MapAssetInstance } from "@/hooks/world/useMapInstancingData";
|
||||||
|
|
||||||
export function PathSystem(): React.JSX.Element | null {
|
export function PathSystem(): React.JSX.Element | null {
|
||||||
if (!PATH_DEBUG_PREVIEW_ENABLED && !PATH_TILE_RENDER_ENABLED) {
|
if (!PATH_DEBUG_PREVIEW_ENABLED && !PATH_TILE_RENDER_ENABLED) {
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
|
|||||||
import { useTerrainSurfaceData } from "@/hooks/world/useTerrainSurfaceData";
|
import { useTerrainSurfaceData } from "@/hooks/world/useTerrainSurfaceData";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
import { sampleTerrainSurfaceAtXZ } from "@/utils/world/terrainSurfaceSampler";
|
import { sampleTerrainSurfaceAtXZ } from "@/utils/world/terrainSurfaceSampler";
|
||||||
import type { MapAssetInstance } from "@/world/map-instancing/useMapInstancingData";
|
import type { MapAssetInstance } from "@/hooks/world/useMapInstancingData";
|
||||||
import {
|
import {
|
||||||
PATH_TILE_MAX_COUNT,
|
PATH_TILE_MAX_COUNT,
|
||||||
PATH_SURFACE_KEY,
|
PATH_SURFACE_KEY,
|
||||||
PATH_TILE_ROTATION,
|
PATH_TILE_ROTATION,
|
||||||
PATH_TILE_SAMPLE_STEP,
|
PATH_TILE_SAMPLE_STEP,
|
||||||
PATH_TILE_SCALE,
|
PATH_TILE_SCALE,
|
||||||
} from "@/world/paths/pathConfig";
|
} from "@/data/world/pathConfig";
|
||||||
|
|
||||||
function createSampleCenters(min: number, max: number, step: number): number[] {
|
function createSampleCenters(min: number, max: number, step: number): number[] {
|
||||||
const start = Math.ceil(min / step) * step + step * 0.5;
|
const start = Math.ceil(min / step) * step + step * 0.5;
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ interface VegetationWindUniforms {
|
|||||||
noiseScale: { value: number };
|
noiseScale: { value: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const meshDataCache = new Map<string, MeshData[]>();
|
||||||
|
|
||||||
function updateVegetationWindUniforms(
|
function updateVegetationWindUniforms(
|
||||||
uniforms: VegetationWindUniforms,
|
uniforms: VegetationWindUniforms,
|
||||||
elapsedTime: number,
|
elapsedTime: number,
|
||||||
@@ -242,9 +244,14 @@ export function InstancedVegetation({
|
|||||||
const windUniformsRef = useRef<VegetationWindUniforms[]>([]);
|
const windUniformsRef = useRef<VegetationWindUniforms[]>([]);
|
||||||
|
|
||||||
const meshDataList = useMemo(() => {
|
const meshDataList = useMemo(() => {
|
||||||
|
const cached = meshDataCache.get(modelPath);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
optimizeGLTFSceneTextures(scene, maxAnisotropy);
|
||||||
return extractMeshes(scene);
|
const extracted = extractMeshes(scene);
|
||||||
}, [maxAnisotropy, scene]);
|
meshDataCache.set(modelPath, extracted);
|
||||||
|
return extracted;
|
||||||
|
}, [maxAnisotropy, modelPath, scene]);
|
||||||
const groundedInstances = useMemo(
|
const groundedInstances = useMemo(
|
||||||
() =>
|
() =>
|
||||||
instances.map((instance) => {
|
instances.map((instance) => {
|
||||||
@@ -325,20 +332,8 @@ export function InstancedVegetation({
|
|||||||
(uniforms): uniforms is VegetationWindUniforms =>
|
(uniforms): uniforms is VegetationWindUniforms =>
|
||||||
uniforms !== undefined,
|
uniforms !== undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
windUniformsRef.current = [];
|
windUniformsRef.current = [];
|
||||||
|
|
||||||
for (const meshData of meshDataList) {
|
|
||||||
meshData.geometry.dispose();
|
|
||||||
if (Array.isArray(meshData.material)) {
|
|
||||||
for (const mat of meshData.material) {
|
|
||||||
mat.dispose();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
meshData.material.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [meshDataList]);
|
}, [meshDataList]);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
VEGETATION_TYPE_KEYS,
|
VEGETATION_TYPE_KEYS,
|
||||||
VEGETATION_TYPES,
|
VEGETATION_TYPES,
|
||||||
type VegetationType,
|
type VegetationType,
|
||||||
} from "@/world/vegetation/vegetationConfig";
|
} from "@/data/world/vegetationConfig";
|
||||||
|
|
||||||
interface VegetationChunk {
|
interface VegetationChunk {
|
||||||
key: string;
|
key: string;
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import type { MapNode } from "@/types/editor/editor";
|
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
|
||||||
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
|
||||||
import { INSTANCED_MAP_EXCEPTIONS } from "@/world/vegetation/vegetationConfig";
|
|
||||||
|
|
||||||
export interface VegetationInstance {
|
|
||||||
position: Vector3Tuple;
|
|
||||||
rotation: Vector3Tuple;
|
|
||||||
scale: Vector3Tuple;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InstancedMapEntry {
|
|
||||||
modelPath: string;
|
|
||||||
instances: VegetationInstance[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VegetationData = Map<string, InstancedMapEntry>;
|
|
||||||
|
|
||||||
function mapNodeToInstance(node: MapNode): VegetationInstance {
|
|
||||||
return {
|
|
||||||
position: node.position,
|
|
||||||
rotation: node.rotation,
|
|
||||||
scale: node.scale,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractVegetationData(
|
|
||||||
mapNodes: MapNode[],
|
|
||||||
models: Map<string, string>,
|
|
||||||
): VegetationData {
|
|
||||||
const data: VegetationData = new Map();
|
|
||||||
|
|
||||||
for (const node of mapNodes) {
|
|
||||||
if (node.type !== "Object3D") continue;
|
|
||||||
if (INSTANCED_MAP_EXCEPTIONS.has(node.name)) continue;
|
|
||||||
|
|
||||||
const modelPath = models.get(node.name);
|
|
||||||
if (!modelPath) continue;
|
|
||||||
|
|
||||||
const entry = data.get(node.name);
|
|
||||||
|
|
||||||
if (entry) {
|
|
||||||
entry.instances.push(mapNodeToInstance(node));
|
|
||||||
} else {
|
|
||||||
data.set(node.name, {
|
|
||||||
modelPath,
|
|
||||||
instances: [mapNodeToInstance(node)],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVegetationData(): {
|
|
||||||
data: VegetationData | null;
|
|
||||||
isLoading: boolean;
|
|
||||||
} {
|
|
||||||
const [data, setData] = useState<VegetationData | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
const sceneData = await loadMapSceneData();
|
|
||||||
|
|
||||||
if (!cancelled && sceneData) {
|
|
||||||
setData(extractVegetationData(sceneData.mapNodes, sceneData.models));
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
load();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { data, isLoading };
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import { useMemo, useRef } from "react";
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { FOG_CONFIG } from "@/data/world/fogConfig";
|
import { FOG_CONFIG } from "@/data/world/fogConfig";
|
||||||
import { getWindVector } from "@/data/world/windConfig";
|
import { getWindVector } from "@/utils/world/windVector";
|
||||||
import { WATER_SHADER_CONFIG } from "@/data/world/waterConfig";
|
import { WATER_SHADER_CONFIG } from "@/data/world/waterConfig";
|
||||||
import type { WaterSurfaceConfig } from "@/data/world/waterConfig";
|
import type { WaterSurfaceConfig } from "@/data/world/waterConfig";
|
||||||
import { useWind } from "@/hooks/world/useWind";
|
import { useWind } from "@/hooks/world/useWind";
|
||||||
|
|||||||
@@ -6,12 +6,17 @@
|
|||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"ignoreDeprecations": "6.0",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
|
|||||||
Reference in New Issue
Block a user