Compare commits
5 Commits
ae34dc38ed
...
f567540f22
| Author | SHA1 | Date | |
|---|---|---|---|
| f567540f22 | |||
| 688302d985 | |||
| f9d7c3f00e | |||
| 28c6ef199f | |||
| ff79448ce8 |
@@ -0,0 +1,226 @@
|
|||||||
|
# Game States & Substates
|
||||||
|
|
||||||
|
Documentation technique pour le testing et debugging du flow de jeu.
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
```
|
||||||
|
intro ──► bike ──► pylone ──► ferme ──► outro
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main States
|
||||||
|
|
||||||
|
| State | Description |
|
||||||
|
| -------- | ------------------------------------------------------------------ |
|
||||||
|
| `intro` | Séquence d'introduction (cinématique, naming, premier déplacement) |
|
||||||
|
| `bike` | Mission de réparation du vélo |
|
||||||
|
| `pylone` | Quête du pylone (alert → searching → helped → manipulation) |
|
||||||
|
| `ferme` | Mission de réparation de la ferme |
|
||||||
|
| `outro` | Fin du jeu |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intro (GameStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ intro.currentStep │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
intro ──► sequence_video ──► naming ──► start-move ──► bike
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étapes
|
||||||
|
|
||||||
|
| Step | Trigger | Action | Passage vers |
|
||||||
|
| ---------------- | -------- | ---------------------------------------------- | ------------------------------------------------ |
|
||||||
|
| `intro` | Initial | État initial | Auto → `sequence_video` quand `sceneReady: true` |
|
||||||
|
| `sequence_video` | GameFlow | Déclenche la cinématique dans `GameCinematics` | Fin cinématique → `naming` |
|
||||||
|
| `naming` | IntroUI | Affiche l'input pour le prénom | Submit → `start-move` |
|
||||||
|
| `start-move` | GameFlow | Active le mouvement (`canMove: true`) | Via zone "fabrikExit" → `bike` |
|
||||||
|
|
||||||
|
### Transition vers bike
|
||||||
|
|
||||||
|
- **Trigger**: Zone `fabrikExit` dans `zones.ts`
|
||||||
|
- **Action**: `advanceGameState()` dans `ZoneDetection.tsx`
|
||||||
|
- **Résultat**: `mainState: "bike"`, `intro.hasCompleted: true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bike (MissionStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ bike.currentStep (MissionStep) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
locked ──► waiting ──► inspected ──► fragmented ──► scanning ──► repairing ──► reassembling ──► done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transition vers pylone
|
||||||
|
|
||||||
|
- **Trigger**: `bike.currentStep: "done"`
|
||||||
|
- **Action**: `completeBikeState()` dans useGameStore
|
||||||
|
- **Résultat**: `mainState: "pylone"`, `pylone.currentStep: "locked"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pylone (PyloneStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ pylone.currentStep (PyloneStep) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
locked (bypass) ──► alert ──► searching ──► helped ──► manipulation ──► outro
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étapes
|
||||||
|
|
||||||
|
| Step | Trigger | Action | Passage vers |
|
||||||
|
| -------------- | ---------------------------------- | ---------------------------------- | --------------------------------------------------------- |
|
||||||
|
| `locked` | Initial (après bike) | État initial | **Bypass automatique** → `alert` via `advanceGameState()` |
|
||||||
|
| `alert` | `advanceGameState()` | Affiche l'alerte (à implémenter) | Via `advanceGameState()` |
|
||||||
|
| `searching` | `advanceGameState()` | Déclenché par zone "searchingZone" | Via `advanceGameState()` |
|
||||||
|
| `helped` | Interaction avec `NPCHelper` | Dialogue avec le villageois | Via interaction 3D |
|
||||||
|
| `manipulation` | Interaction avec `PyloneDestroyed` | Interaction avec l'objet central | Via `advanceGameState()` → `outro` |
|
||||||
|
|
||||||
|
### Bypass automatique
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// useGameStore.ts - advancePyloneStep()
|
||||||
|
if (state.pylone.currentStep === "locked") {
|
||||||
|
return { pylone: { ...state.pylone, currentStep: "alert" } };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transition vers outro
|
||||||
|
|
||||||
|
- **Trigger**: `pylone.currentStep: "manipulation"` + `advanceGameState()`
|
||||||
|
- **Action**: `advancePyloneStep()` détecte fin de la séquence
|
||||||
|
- **Résultat**: `mainState: "outro"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ferme (MissionStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ferme.currentStep (MissionStep) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
locked ──► waiting ──► inspected ──► fragmented ──► scanning ──► repairing ──► reassembling ──► done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transition vers outro
|
||||||
|
|
||||||
|
- **Trigger**: `ferme.currentStep: "done"`
|
||||||
|
- **Action**: `completeFermeState()` dans useGameStore
|
||||||
|
- **Résultat**: `mainState: "outro"`, `ferme.irrigationFixed: true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Outro
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ outro.hasStarted │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
waiting ──► started
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug Panel
|
||||||
|
|
||||||
|
Le debug panel permet de tester toutes les transitions :
|
||||||
|
|
||||||
|
### Utilisation
|
||||||
|
|
||||||
|
1. Ouvrir le jeu en mode debug (`Debug: true` dans `Debug.ts`)
|
||||||
|
2. Le panneau "Game State" apparaît en bas à gauche
|
||||||
|
3. **Main state**: Sélectionner le state principal
|
||||||
|
4. **Sub state**: Sélectionner le sub-state
|
||||||
|
5. **Previous/Next step**: Avancer ou reculer d'un step
|
||||||
|
6. **Reset**: Remettre à l'état initial
|
||||||
|
|
||||||
|
### Raccourcis clavier
|
||||||
|
|
||||||
|
| Action | Clavier |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| Avancer | Debug panel button |
|
||||||
|
| Reculer | Debug panel button |
|
||||||
|
| Reset | Debug panel button |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comment tester chaque section
|
||||||
|
|
||||||
|
### Tester l'intro
|
||||||
|
|
||||||
|
1. Vérifier que `sceneReady: false` au démarrage
|
||||||
|
2. Attendre que le loader termine (`sceneReady: true`)
|
||||||
|
3. Vérifier `intro.currentStep: "intro"` → auto vers `sequence_video`
|
||||||
|
4. Si cinématique fonctionne : `sequence_video` → `naming`
|
||||||
|
5. Entrer un prénom : `naming` → `start-move`
|
||||||
|
6. Vérifier `canMove: true` après `start-move`
|
||||||
|
7. Entrer dans la zone `fabrikExit` → `mainState: "bike"`
|
||||||
|
|
||||||
|
### Tester bike
|
||||||
|
|
||||||
|
1. Via debug panel, avancer jusqu'à `done`
|
||||||
|
2. Vérifier `mainState: "pylone"`
|
||||||
|
|
||||||
|
### Tester pylone
|
||||||
|
|
||||||
|
1. Via debug panel, avancer (bypass `locked` → `alert`)
|
||||||
|
2. Vérifier `pylone.currentStep: "alert"`
|
||||||
|
3. Avancer : `alert` → `searching` → `helped` → `manipulation`
|
||||||
|
4. Après `manipulation`, vérifier `mainState: "outro"`
|
||||||
|
|
||||||
|
### Tester ferme
|
||||||
|
|
||||||
|
1. Via debug panel, avancer dans bike jusqu'à `done`
|
||||||
|
2. Vérifier `mainState: "pylone"` → `ferme` (après pylone)
|
||||||
|
3. Avancer jusqu'à `done`
|
||||||
|
4. Vérifier `mainState: "outro"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fichiers clés
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
| ------------------------------------------------- | ------------------------------------- |
|
||||||
|
| `src/managers/stores/useGameStore.ts` | Store Zustand avec toutes les actions |
|
||||||
|
| `src/types/game.ts` | Définition de `GameStep` |
|
||||||
|
| `src/types/gameplay/pylone.ts` | Définition de `PyloneStep` |
|
||||||
|
| `src/types/gameplay/repairMission.ts` | Définition de `MissionStep` |
|
||||||
|
| `src/components/game/GameFlow.tsx` | Logique de transition de l'intro |
|
||||||
|
| `src/components/zone/ZoneDetection.tsx` | Déclenchement des zones |
|
||||||
|
| `src/components/ui/debug/GameStateDebugPanel.tsx` | Outil de debug |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## État initial
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
mainState: "intro",
|
||||||
|
isCinematicPlaying: false,
|
||||||
|
sceneReady: false,
|
||||||
|
missionFlow: {
|
||||||
|
activityCity: true,
|
||||||
|
canMove: false,
|
||||||
|
dialogMessage: null,
|
||||||
|
playerName: "",
|
||||||
|
},
|
||||||
|
intro: { currentStep: "intro", dialogueAudio: null, hasCompleted: false, isBikeUnlocked: false },
|
||||||
|
bike: { currentStep: "locked", dialogueAudio: null, isRepaired: false },
|
||||||
|
pylone: { currentStep: "locked", dialogueAudio: null, isPowered: false },
|
||||||
|
ferme: { currentStep: "locked", dialogueAudio: null, irrigationFixed: false },
|
||||||
|
outro: { dialogueAudio: null, hasStarted: false },
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,6 +1,21 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"cinematics": [
|
"cinematics": [
|
||||||
|
{
|
||||||
|
"id": "intro_sequence",
|
||||||
|
"trigger": "intro_sequence",
|
||||||
|
"cameraKeyframes": [
|
||||||
|
{ "time": 0, "position": [8, 5, 12], "target": [0, 2, 0] },
|
||||||
|
{ "time": 8, "position": [12, 4, -6], "target": [10, 1.4, -8] },
|
||||||
|
{ "time": 16, "position": [5, 6, -15], "target": [0, 3, -20] },
|
||||||
|
{ "time": 24, "position": [0, 8, -30], "target": [0, 0, -40] }
|
||||||
|
],
|
||||||
|
"dialogueCues": [
|
||||||
|
{ "time": 0, "dialogueId": "intro_welcome" },
|
||||||
|
{ "time": 8, "dialogueId": "intro_explanation" },
|
||||||
|
{ "time": 16, "dialogueId": "intro_mission" }
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "intro_overview",
|
"id": "intro_overview",
|
||||||
"timecode": 0,
|
"timecode": 0,
|
||||||
|
|||||||
@@ -1,64 +1,34 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { AudioManager } from "@/managers/AudioManager";
|
|
||||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
import { AUDIO_PATHS } from "@/data/audioConfig";
|
|
||||||
|
|
||||||
export function GameFlow(): null {
|
export function GameFlow(): null {
|
||||||
const step = useGameStore((state) => state.intro.currentStep);
|
const step = useGameStore((state) => state.intro.currentStep);
|
||||||
const setStep = useGameStore((state) => state.setIntroStep);
|
const setStep = useGameStore((state) => state.setIntroStep);
|
||||||
const setActivityCity = useGameStore((state) => state.setActivityCity);
|
const isCinematicPlaying = useGameStore((state) => state.isCinematicPlaying);
|
||||||
|
const sceneReady = useGameStore((state) => state.sceneReady);
|
||||||
const setCanMove = useGameStore((state) => state.setCanMove);
|
const setCanMove = useGameStore((state) => state.setCanMove);
|
||||||
const hasInitialized = useRef(false);
|
const hasInitialized = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasInitialized.current && step === "intro") {
|
if (!hasInitialized.current && step === "intro" && sceneReady) {
|
||||||
hasInitialized.current = true;
|
hasInitialized.current = true;
|
||||||
setStep("start-intro");
|
setStep("sequence_video");
|
||||||
}
|
}
|
||||||
}, [step, setStep]);
|
}, [step, setStep, sceneReady]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (step === "start-intro") {
|
if (step === "sequence_video" && !isCinematicPlaying) {
|
||||||
const audio = AudioManager.getInstance();
|
|
||||||
audio.playSoundWithCallback(AUDIO_PATHS.intro, 0.5, () => {
|
|
||||||
setStep("naming");
|
setStep("naming");
|
||||||
});
|
|
||||||
|
|
||||||
return () => {};
|
|
||||||
}
|
}
|
||||||
|
}, [step, isCinematicPlaying, setStep]);
|
||||||
|
|
||||||
if (step === "bienvenue") {
|
useEffect(() => {
|
||||||
const audio = AudioManager.getInstance();
|
if (step === "start-move") {
|
||||||
audio.playSoundWithCallback(AUDIO_PATHS.bienvenue, 0.5, () => {
|
|
||||||
setCanMove(true);
|
setCanMove(true);
|
||||||
setStep("star-move");
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === "mission2") {
|
|
||||||
setActivityCity(false);
|
|
||||||
const audio = AudioManager.getInstance();
|
|
||||||
audio.playSound(AUDIO_PATHS.alertCentral, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === "searching") {
|
|
||||||
const audio = AudioManager.getInstance();
|
|
||||||
audio.playSound(AUDIO_PATHS.searching, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === "helped") {
|
|
||||||
const audio = AudioManager.getInstance();
|
|
||||||
audio.playSound(AUDIO_PATHS.helped, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === "manipulation") {
|
|
||||||
setCanMove(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [step, setStep, setActivityCity, setCanMove]);
|
}, [step, setCanMove]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ interface NPCHelperProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NPCHelper({ position }: NPCHelperProps): React.JSX.Element {
|
export function NPCHelper({ position }: NPCHelperProps): React.JSX.Element {
|
||||||
const step = useGameStore((state) => state.intro.currentStep);
|
const step = useGameStore((state) => state.pylone.currentStep);
|
||||||
const setStep = useGameStore((state) => state.setIntroStep);
|
const setPyloneStep = useGameStore((state) => state.setPyloneState);
|
||||||
const debug = Debug.getInstance();
|
const debug = Debug.getInstance();
|
||||||
|
|
||||||
const handlePress = (): void => {
|
const handlePress = (): void => {
|
||||||
if (step === "searching") {
|
if (step === "searching") {
|
||||||
setStep("helped");
|
setPyloneStep({ currentStep: "helped" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldShow = step === "searching" || debug.active;
|
const shouldShow = step === "searching" || step === "helped" || debug.active;
|
||||||
|
|
||||||
if (!shouldShow) {
|
if (!shouldShow) {
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ interface PyloneDestroyedProps {
|
|||||||
export function PyloneDestroyed({
|
export function PyloneDestroyed({
|
||||||
position,
|
position,
|
||||||
}: PyloneDestroyedProps): React.JSX.Element {
|
}: PyloneDestroyedProps): React.JSX.Element {
|
||||||
const step = useGameStore((state) => state.intro.currentStep);
|
const step = useGameStore((state) => state.pylone.currentStep);
|
||||||
const setStep = useGameStore((state) => state.setIntroStep);
|
const setPyloneStep = useGameStore((state) => state.setPyloneState);
|
||||||
const setCanMove = useGameStore((state) => state.setCanMove);
|
const setCanMove = useGameStore((state) => state.setCanMove);
|
||||||
const showDialog = useGameStore((state) => state.showDialog);
|
const showDialog = useGameStore((state) => state.showDialog);
|
||||||
const debug = Debug.getInstance();
|
const debug = Debug.getInstance();
|
||||||
@@ -19,7 +19,7 @@ export function PyloneDestroyed({
|
|||||||
const handlePress = (): void => {
|
const handlePress = (): void => {
|
||||||
if (step === "helped") {
|
if (step === "helped") {
|
||||||
setCanMove(false);
|
setCanMove(false);
|
||||||
setStep("manipulation");
|
setPyloneStep({ currentStep: "manipulation" });
|
||||||
} else if (step === "searching") {
|
} else if (step === "searching") {
|
||||||
showDialog(
|
showDialog(
|
||||||
"Cet objet est trop lourd pour le porter tout seul, trouve de l'aide",
|
"Cet objet est trop lourd pour le porter tout seul, trouve de l'aide",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function IntroUI(): React.JSX.Element | null {
|
|||||||
if (inputValue.trim() === "") return;
|
if (inputValue.trim() === "") return;
|
||||||
|
|
||||||
setPlayerName(inputValue.trim());
|
setPlayerName(inputValue.trim());
|
||||||
setStep("bienvenue");
|
setStep("start-move");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent): void => {
|
const handleKeyDown = (e: React.KeyboardEvent): void => {
|
||||||
@@ -100,7 +100,7 @@ export function BienvenueDisplay(): React.JSX.Element | null {
|
|||||||
const step = useGameStore((state) => state.intro.currentStep);
|
const step = useGameStore((state) => state.intro.currentStep);
|
||||||
const playerName = useGameStore((state) => state.missionFlow.playerName);
|
const playerName = useGameStore((state) => state.missionFlow.playerName);
|
||||||
|
|
||||||
if (step !== "bienvenue") return null;
|
if (step !== "start-move") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "@/managers/stores/useGameStore";
|
} from "@/managers/stores/useGameStore";
|
||||||
import { isMissionStep, MISSION_STEPS } from "@/types/gameplay/repairMission";
|
import { isMissionStep, MISSION_STEPS } from "@/types/gameplay/repairMission";
|
||||||
import { GAME_STEPS, type GameStep } from "@/types/game";
|
import { GAME_STEPS, type GameStep } from "@/types/game";
|
||||||
|
import { PYLONE_STEPS, type PyloneStep } from "@/types/gameplay/pylone";
|
||||||
|
|
||||||
const MAIN_STATES: MainGameState[] = [
|
const MAIN_STATES: MainGameState[] = [
|
||||||
"intro",
|
"intro",
|
||||||
@@ -54,6 +55,8 @@ export function GameStateDebugPanel(): React.JSX.Element {
|
|||||||
const subStateOptions =
|
const subStateOptions =
|
||||||
mainState === "intro"
|
mainState === "intro"
|
||||||
? GAME_STEPS
|
? GAME_STEPS
|
||||||
|
: mainState === "pylone"
|
||||||
|
? PYLONE_STEPS
|
||||||
: mainState === "outro"
|
: mainState === "outro"
|
||||||
? ["waiting", "started"]
|
? ["waiting", "started"]
|
||||||
: MISSION_STEPS;
|
: MISSION_STEPS;
|
||||||
@@ -64,6 +67,11 @@ export function GameStateDebugPanel(): React.JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mainState === "pylone") {
|
||||||
|
setPyloneState({ currentStep: nextSubState as PyloneStep });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (mainState === "outro") {
|
if (mainState === "outro") {
|
||||||
setOutroState({ hasStarted: nextSubState === "started" });
|
setOutroState({ hasStarted: nextSubState === "started" });
|
||||||
return;
|
return;
|
||||||
@@ -76,11 +84,6 @@ export function GameStateDebugPanel(): React.JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainState === "pylone") {
|
|
||||||
setPyloneState({ currentStep: nextSubState });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mainState === "ferme") {
|
if (mainState === "ferme") {
|
||||||
setFermeState({ currentStep: nextSubState });
|
setFermeState({ currentStep: nextSubState });
|
||||||
return;
|
return;
|
||||||
@@ -95,11 +98,6 @@ export function GameStateDebugPanel(): React.JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextMainState === "pylone" && pyloneStep === "locked") {
|
|
||||||
setPyloneState({ currentStep: "waiting" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextMainState === "ferme" && fermeStep === "locked") {
|
if (nextMainState === "ferme" && fermeStep === "locked") {
|
||||||
setFermeState({ currentStep: "waiting" });
|
setFermeState({ currentStep: "waiting" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ export function ZoneDetection(): null {
|
|||||||
const triggeredZones = useRef<Set<string>>(new Set());
|
const triggeredZones = useRef<Set<string>>(new Set());
|
||||||
const debug = Debug.getInstance();
|
const debug = Debug.getInstance();
|
||||||
const step = useGameStore((state) => state.intro.currentStep);
|
const step = useGameStore((state) => state.intro.currentStep);
|
||||||
|
const mainState = useGameStore((state) => state.mainState);
|
||||||
const setStep = useGameStore((state) => state.setIntroStep);
|
const setStep = useGameStore((state) => state.setIntroStep);
|
||||||
|
const setPyloneStep = useGameStore((state) => state.setPyloneState);
|
||||||
|
const advanceGameState = useGameStore((state) => state.advanceGameState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!debug.active) return;
|
if (!debug.active) return;
|
||||||
@@ -65,7 +68,11 @@ export function ZoneDetection(): null {
|
|||||||
const distanceSq = _playerPos.distanceToSquared(_zonePos);
|
const distanceSq = _playerPos.distanceToSquared(_zonePos);
|
||||||
|
|
||||||
if (distanceSq <= zone.radius * zone.radius) {
|
if (distanceSq <= zone.radius * zone.radius) {
|
||||||
|
if (zone.targetStep === "bike" && mainState === "intro") {
|
||||||
|
advanceGameState();
|
||||||
|
} else {
|
||||||
setStep(zone.targetStep);
|
setStep(zone.targetStep);
|
||||||
|
}
|
||||||
triggeredZones.current.add(zone.id);
|
triggeredZones.current.add(zone.id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
|||||||
description:
|
description:
|
||||||
"Repair the damaged cooling module before relaunching the bike",
|
"Repair the damaged cooling module before relaunching the bike",
|
||||||
modelPath: "/models/ebike/model.gltf",
|
modelPath: "/models/ebike/model.gltf",
|
||||||
modelScale: 0.50,
|
modelScale: 0.5,
|
||||||
stageUiPath: "/assets/UI/ebike.webm",
|
stageUiPath: "/assets/UI/ebike.webm",
|
||||||
interactUiPath: REPAIR_INTERACT_UI_PATH,
|
interactUiPath: REPAIR_INTERACT_UI_PATH,
|
||||||
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
||||||
@@ -85,51 +85,6 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
pylone: {
|
|
||||||
id: "pylone",
|
|
||||||
label: "Power pylon",
|
|
||||||
description:
|
|
||||||
"Restore the pylon lamp relay and damaged panel before reconnecting the grid",
|
|
||||||
modelPath: "/models/pylone/model.gltf",
|
|
||||||
stageUiPath: "/assets/UI/centrale.webm",
|
|
||||||
interactUiPath: REPAIR_INTERACT_UI_PATH,
|
|
||||||
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
|
||||||
case: DEFAULT_REPAIR_CASE,
|
|
||||||
reassemblySeconds: 1.8,
|
|
||||||
requiredReplacementPartId: "pylone-grid-relay-replacement",
|
|
||||||
scanPartSeconds: 1.4,
|
|
||||||
brokenParts: [
|
|
||||||
{
|
|
||||||
id: "pylone-grid-relay",
|
|
||||||
label: "Grid relay",
|
|
||||||
nodeName: "lampe",
|
|
||||||
placeholderName: "placeholder_1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pylone-damaged-panel",
|
|
||||||
label: "Damaged solar panel",
|
|
||||||
nodeName: "panneau2",
|
|
||||||
placeholderName: "placeholder_2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
replacementParts: [
|
|
||||||
{
|
|
||||||
id: "pylone-grid-relay-replacement",
|
|
||||||
label: "Replacement grid relay",
|
|
||||||
modelPath: "/models/pylone/model.gltf",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pylone-stone-decoy",
|
|
||||||
label: "Stone counterweight",
|
|
||||||
modelPath: "/models/galet/model.gltf",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pylone-cooling-decoy",
|
|
||||||
label: "Cooling core",
|
|
||||||
modelPath: "/models/refroidisseur/model.gltf",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
ferme: {
|
ferme: {
|
||||||
id: "ferme",
|
id: "ferme",
|
||||||
label: "Vertical farm",
|
label: "Vertical farm",
|
||||||
|
|||||||
+2
-9
@@ -1,4 +1,4 @@
|
|||||||
import type { Zone } from "@/types/game";
|
import type { Zone, GameStep } from "@/types/game";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
export const ZONES: Zone[] = [
|
export const ZONES: Zone[] = [
|
||||||
@@ -7,13 +7,6 @@ export const ZONES: Zone[] = [
|
|||||||
position: [-5, 25, -15] as Vector3Tuple,
|
position: [-5, 25, -15] as Vector3Tuple,
|
||||||
radius: 10,
|
radius: 10,
|
||||||
height: 20,
|
height: 20,
|
||||||
targetStep: "mission2",
|
targetStep: "bike" as GameStep,
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "searchingZone",
|
|
||||||
position: [-5, 25, -30] as Vector3Tuple,
|
|
||||||
radius: 10,
|
|
||||||
height: 20,
|
|
||||||
targetStep: "searching",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import { useGameStore } from "@/managers/stores/useGameStore";
|
|||||||
import type { MissionStep } from "@/types/gameplay/repairMission";
|
import type { MissionStep } from "@/types/gameplay/repairMission";
|
||||||
|
|
||||||
export function useRepairMovementLocked(): boolean {
|
export function useRepairMovementLocked(): boolean {
|
||||||
return false;
|
|
||||||
|
|
||||||
return useGameStore((state) => {
|
return useGameStore((state) => {
|
||||||
switch (state.mainState) {
|
switch (state.mainState) {
|
||||||
case "bike":
|
case "bike":
|
||||||
return isRepairMovementLocked(state.bike.currentStep);
|
return isRepairMovementLocked(state.bike.currentStep);
|
||||||
case "pylone":
|
case "pylone":
|
||||||
return isRepairMovementLocked(state.pylone.currentStep);
|
return state.pylone.currentStep === "manipulation";
|
||||||
case "ferme":
|
case "ferme":
|
||||||
return isRepairMovementLocked(state.ferme.currentStep);
|
return isRepairMovementLocked(state.ferme.currentStep);
|
||||||
case "intro":
|
case "intro":
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import {
|
|||||||
type MissionStep,
|
type MissionStep,
|
||||||
type RepairMissionId,
|
type RepairMissionId,
|
||||||
} from "@/types/gameplay/repairMission";
|
} from "@/types/gameplay/repairMission";
|
||||||
|
import { type PyloneStep } from "@/types/gameplay/pylone";
|
||||||
|
|
||||||
export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro";
|
export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro";
|
||||||
export type { MissionStep, RepairMissionId };
|
export type { MissionStep, RepairMissionId, PyloneStep };
|
||||||
|
|
||||||
interface IntroState {
|
interface IntroState {
|
||||||
currentStep: GameStep;
|
currentStep: GameStep;
|
||||||
@@ -33,12 +34,15 @@ interface MissionFlowState {
|
|||||||
interface GameState {
|
interface GameState {
|
||||||
mainState: MainGameState;
|
mainState: MainGameState;
|
||||||
isCinematicPlaying: boolean;
|
isCinematicPlaying: boolean;
|
||||||
|
sceneReady: boolean;
|
||||||
missionFlow: MissionFlowState;
|
missionFlow: MissionFlowState;
|
||||||
intro: IntroState;
|
intro: IntroState;
|
||||||
bike: MissionState & {
|
bike: MissionState & {
|
||||||
isRepaired: boolean;
|
isRepaired: boolean;
|
||||||
};
|
};
|
||||||
pylone: MissionState & {
|
pylone: {
|
||||||
|
currentStep: PyloneStep;
|
||||||
|
dialogueAudio: string | null;
|
||||||
isPowered: boolean;
|
isPowered: boolean;
|
||||||
};
|
};
|
||||||
ferme: MissionState & {
|
ferme: MissionState & {
|
||||||
@@ -53,6 +57,7 @@ interface GameState {
|
|||||||
interface GameActions {
|
interface GameActions {
|
||||||
setMainState: (mainState: MainGameState) => void;
|
setMainState: (mainState: MainGameState) => void;
|
||||||
setCinematicPlaying: (isCinematicPlaying: boolean) => void;
|
setCinematicPlaying: (isCinematicPlaying: boolean) => void;
|
||||||
|
setSceneReady: (sceneReady: boolean) => void;
|
||||||
hideDialog: () => void;
|
hideDialog: () => void;
|
||||||
setActivityCity: (activityCity: boolean) => void;
|
setActivityCity: (activityCity: boolean) => void;
|
||||||
setCanMove: (canMove: boolean) => void;
|
setCanMove: (canMove: boolean) => void;
|
||||||
@@ -66,7 +71,6 @@ interface GameActions {
|
|||||||
setMissionStep: (mission: RepairMissionId, step: MissionStep) => void;
|
setMissionStep: (mission: RepairMissionId, step: MissionStep) => void;
|
||||||
completeIntro: () => void;
|
completeIntro: () => void;
|
||||||
completeBike: () => void;
|
completeBike: () => void;
|
||||||
completePylone: () => void;
|
|
||||||
completeFerme: () => void;
|
completeFerme: () => void;
|
||||||
completeMission: (mission: RepairMissionId) => void;
|
completeMission: (mission: RepairMissionId) => void;
|
||||||
startOutro: () => void;
|
startOutro: () => void;
|
||||||
@@ -104,22 +108,7 @@ function completeBikeState(state: GameState): GameStateUpdate {
|
|||||||
},
|
},
|
||||||
pylone: {
|
pylone: {
|
||||||
...state.pylone,
|
...state.pylone,
|
||||||
currentStep: "waiting",
|
currentStep: "locked",
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function completePyloneState(state: GameState): GameStateUpdate {
|
|
||||||
return {
|
|
||||||
mainState: "ferme",
|
|
||||||
pylone: {
|
|
||||||
...state.pylone,
|
|
||||||
currentStep: "done",
|
|
||||||
isPowered: true,
|
|
||||||
},
|
|
||||||
ferme: {
|
|
||||||
...state.ferme,
|
|
||||||
currentStep: "waiting",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -159,13 +148,48 @@ function completeMissionState(
|
|||||||
switch (mission) {
|
switch (mission) {
|
||||||
case "bike":
|
case "bike":
|
||||||
return completeBikeState(state);
|
return completeBikeState(state);
|
||||||
case "pylone":
|
|
||||||
return completePyloneState(state);
|
|
||||||
case "ferme":
|
case "ferme":
|
||||||
return completeFermeState(state);
|
return completeFermeState(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNextPyloneStep(step: PyloneStep): PyloneStep {
|
||||||
|
switch (step) {
|
||||||
|
case "locked":
|
||||||
|
return "alert";
|
||||||
|
case "alert":
|
||||||
|
return "searching";
|
||||||
|
case "searching":
|
||||||
|
return "helped";
|
||||||
|
case "helped":
|
||||||
|
return "manipulation";
|
||||||
|
case "manipulation":
|
||||||
|
return "manipulation";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function advancePyloneStep(state: GameState): GameStateUpdate {
|
||||||
|
if (state.pylone.currentStep === "locked") {
|
||||||
|
return {
|
||||||
|
pylone: { ...state.pylone, currentStep: "alert" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = getNextPyloneStep(state.pylone.currentStep);
|
||||||
|
if (
|
||||||
|
nextStep === "manipulation" &&
|
||||||
|
state.pylone.currentStep === "manipulation"
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
mainState: "outro",
|
||||||
|
pylone: { ...state.pylone, currentStep: "manipulation" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pylone: { ...state.pylone, currentStep: nextStep },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function advanceRepairMissionState(
|
function advanceRepairMissionState(
|
||||||
state: GameState,
|
state: GameState,
|
||||||
mission: RepairMissionId,
|
mission: RepairMissionId,
|
||||||
@@ -203,6 +227,7 @@ function createInitialGameState(): GameState {
|
|||||||
return {
|
return {
|
||||||
mainState: "intro",
|
mainState: "intro",
|
||||||
isCinematicPlaying: false,
|
isCinematicPlaying: false,
|
||||||
|
sceneReady: false,
|
||||||
missionFlow: {
|
missionFlow: {
|
||||||
activityCity: true,
|
activityCity: true,
|
||||||
canMove: false,
|
canMove: false,
|
||||||
@@ -241,6 +266,7 @@ export const useGameStore = create<GameStore>()((set) => ({
|
|||||||
...createInitialGameState(),
|
...createInitialGameState(),
|
||||||
setMainState: (mainState) => set({ mainState }),
|
setMainState: (mainState) => set({ mainState }),
|
||||||
setCinematicPlaying: (isCinematicPlaying) => set({ isCinematicPlaying }),
|
setCinematicPlaying: (isCinematicPlaying) => set({ isCinematicPlaying }),
|
||||||
|
setSceneReady: (sceneReady) => set({ sceneReady }),
|
||||||
hideDialog: () =>
|
hideDialog: () =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
missionFlow: { ...state.missionFlow, dialogMessage: null },
|
missionFlow: { ...state.missionFlow, dialogMessage: null },
|
||||||
@@ -273,7 +299,6 @@ export const useGameStore = create<GameStore>()((set) => ({
|
|||||||
set((state) => setMissionStepState(state, mission, step)),
|
set((state) => setMissionStepState(state, mission, step)),
|
||||||
completeIntro: () => set(completeIntroState),
|
completeIntro: () => set(completeIntroState),
|
||||||
completeBike: () => set((state) => completeMissionState(state, "bike")),
|
completeBike: () => set((state) => completeMissionState(state, "bike")),
|
||||||
completePylone: () => set((state) => completeMissionState(state, "pylone")),
|
|
||||||
completeFerme: () => set((state) => completeMissionState(state, "ferme")),
|
completeFerme: () => set((state) => completeMissionState(state, "ferme")),
|
||||||
completeMission: (mission) =>
|
completeMission: (mission) =>
|
||||||
set((state) => completeMissionState(state, mission)),
|
set((state) => completeMissionState(state, mission)),
|
||||||
@@ -281,9 +306,19 @@ export const useGameStore = create<GameStore>()((set) => ({
|
|||||||
advanceGameState: () =>
|
advanceGameState: () =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (state.mainState === "intro") {
|
if (state.mainState === "intro") {
|
||||||
|
if (state.intro.currentStep === "bike") {
|
||||||
|
return {
|
||||||
|
mainState: "bike",
|
||||||
|
intro: { ...state.intro, hasCompleted: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
return completeIntroState(state);
|
return completeIntroState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.mainState === "pylone") {
|
||||||
|
return advancePyloneStep(state);
|
||||||
|
}
|
||||||
|
|
||||||
if (isRepairMissionId(state.mainState)) {
|
if (isRepairMissionId(state.mainState)) {
|
||||||
return advanceRepairMissionState(state, state.mainState);
|
return advanceRepairMissionState(state, state.mainState);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -19,6 +19,7 @@ export function HomePage(): React.JSX.Element {
|
|||||||
(state) => state.missionFlow.dialogMessage,
|
(state) => state.missionFlow.dialogMessage,
|
||||||
);
|
);
|
||||||
const hideDialog = useGameStore((state) => state.hideDialog);
|
const hideDialog = useGameStore((state) => state.hideDialog);
|
||||||
|
const setSceneReady = useGameStore((state) => state.setSceneReady);
|
||||||
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
||||||
INITIAL_SCENE_LOADING_STATE,
|
INITIAL_SCENE_LOADING_STATE,
|
||||||
);
|
);
|
||||||
@@ -42,13 +43,17 @@ export function HomePage(): React.JSX.Element {
|
|||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nextState.status === "ready" && currentState.status !== "ready") {
|
||||||
|
setSceneReady(true);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...nextState,
|
...nextState,
|
||||||
progress: Math.max(currentState.progress, nextState.progress),
|
progress: Math.max(currentState.progress, nextState.progress),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[],
|
[setSceneReady],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface CinematicDialogueCue {
|
|||||||
export interface CinematicDefinition {
|
export interface CinematicDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
timecode?: number;
|
timecode?: number;
|
||||||
|
trigger?: string;
|
||||||
cameraKeyframes: CinematicCameraKeyframe[];
|
cameraKeyframes: CinematicCameraKeyframe[];
|
||||||
dialogueCues?: CinematicDialogueCue[];
|
dialogueCues?: CinematicDialogueCue[];
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-16
@@ -2,27 +2,17 @@ import type { Vector3Tuple } from "@/types/three/three";
|
|||||||
|
|
||||||
export type GameStep =
|
export type GameStep =
|
||||||
| "intro"
|
| "intro"
|
||||||
| "start-intro"
|
| "sequence_video"
|
||||||
| "naming"
|
| "naming"
|
||||||
| "bienvenue"
|
| "start-move"
|
||||||
| "star-move"
|
| "bike";
|
||||||
| "mission2"
|
|
||||||
| "searching"
|
|
||||||
| "helped"
|
|
||||||
| "manipulation"
|
|
||||||
| "outOfFabrik";
|
|
||||||
|
|
||||||
export const GAME_STEPS: readonly GameStep[] = [
|
export const GAME_STEPS: readonly GameStep[] = [
|
||||||
"intro",
|
"intro",
|
||||||
"start-intro",
|
"sequence_video",
|
||||||
"naming",
|
"naming",
|
||||||
"bienvenue",
|
"start-move",
|
||||||
"star-move",
|
"bike",
|
||||||
"mission2",
|
|
||||||
"searching",
|
|
||||||
"helped",
|
|
||||||
"manipulation",
|
|
||||||
"outOfFabrik",
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export interface Zone {
|
export interface Zone {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export type PyloneStep =
|
||||||
|
| "locked"
|
||||||
|
| "alert"
|
||||||
|
| "searching"
|
||||||
|
| "helped"
|
||||||
|
| "manipulation";
|
||||||
|
|
||||||
|
export const PYLONE_STEPS: readonly PyloneStep[] = [
|
||||||
|
"locked",
|
||||||
|
"alert",
|
||||||
|
"searching",
|
||||||
|
"helped",
|
||||||
|
"manipulation",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function isPyloneStep(value: string): value is PyloneStep {
|
||||||
|
return PYLONE_STEPS.includes(value as PyloneStep);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export type RepairMissionId = "bike" | "pylone" | "ferme";
|
export type RepairMissionId = "bike" | "ferme";
|
||||||
|
|
||||||
export type MissionStep =
|
export type MissionStep =
|
||||||
| "locked"
|
| "locked"
|
||||||
@@ -10,7 +10,7 @@ export type MissionStep =
|
|||||||
| "reassembling"
|
| "reassembling"
|
||||||
| "done";
|
| "done";
|
||||||
|
|
||||||
export const REPAIR_MISSION_IDS = ["bike", "pylone", "ferme"] as const;
|
export const REPAIR_MISSION_IDS = ["bike", "ferme"] as const;
|
||||||
|
|
||||||
export const MISSION_STEPS = [
|
export const MISSION_STEPS = [
|
||||||
"locked",
|
"locked",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function GameCinematics(): null {
|
|||||||
const [dialogueManifest, setDialogueManifest] =
|
const [dialogueManifest, setDialogueManifest] =
|
||||||
useState<DialogueManifest | null>(null);
|
useState<DialogueManifest | null>(null);
|
||||||
const playedCinematicsRef = useRef(new Set<string>());
|
const playedCinematicsRef = useRef(new Set<string>());
|
||||||
|
const triggeredCinematicsRef = useRef(new Set<string>());
|
||||||
const timelineRef = useRef<gsap.core.Timeline | null>(null);
|
const timelineRef = useRef<gsap.core.Timeline | null>(null);
|
||||||
const activeAudiosRef = useRef(new Set<HTMLAudioElement>());
|
const activeAudiosRef = useRef(new Set<HTMLAudioElement>());
|
||||||
const startedAtRef = useRef<number | null>(null);
|
const startedAtRef = useRef<number | null>(null);
|
||||||
@@ -64,7 +65,25 @@ export function GameCinematics(): null {
|
|||||||
|
|
||||||
const elapsedTime = clock.getElapsedTime() - startedAtRef.current;
|
const elapsedTime = clock.getElapsedTime() - startedAtRef.current;
|
||||||
|
|
||||||
|
const currentStep = useGameStore.getState().intro.currentStep;
|
||||||
|
|
||||||
manifest.cinematics.forEach((cinematic) => {
|
manifest.cinematics.forEach((cinematic) => {
|
||||||
|
if (cinematic.trigger) {
|
||||||
|
if (triggeredCinematicsRef.current.has(cinematic.id)) return;
|
||||||
|
if (currentStep !== cinematic.trigger) return;
|
||||||
|
if (cinematic.dialogueCues && !dialogueManifest) return;
|
||||||
|
|
||||||
|
triggeredCinematicsRef.current.add(cinematic.id);
|
||||||
|
playCinematic(camera, cinematic, timelineRef, {
|
||||||
|
dialogueManifest,
|
||||||
|
activeAudiosRef,
|
||||||
|
onComplete: () => {
|
||||||
|
triggeredCinematicsRef.current.delete(cinematic.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (cinematic.timecode === undefined) return;
|
if (cinematic.timecode === undefined) return;
|
||||||
if (cinematic.timecode > elapsedTime) return;
|
if (cinematic.timecode > elapsedTime) return;
|
||||||
if (cinematic.dialogueCues && !dialogueManifest) return;
|
if (cinematic.dialogueCues && !dialogueManifest) return;
|
||||||
@@ -95,6 +114,7 @@ function playCinematic(
|
|||||||
dialogueOptions: {
|
dialogueOptions: {
|
||||||
dialogueManifest: DialogueManifest | null;
|
dialogueManifest: DialogueManifest | null;
|
||||||
activeAudiosRef: MutableRefObject<Set<HTMLAudioElement>>;
|
activeAudiosRef: MutableRefObject<Set<HTMLAudioElement>>;
|
||||||
|
onComplete?: () => void;
|
||||||
},
|
},
|
||||||
): void {
|
): void {
|
||||||
const firstKeyframe = cinematic.cameraKeyframes[0];
|
const firstKeyframe = cinematic.cameraKeyframes[0];
|
||||||
@@ -113,6 +133,7 @@ function playCinematic(
|
|||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
timelineRef.current = null;
|
timelineRef.current = null;
|
||||||
useGameStore.getState().setCinematicPlaying(false);
|
useGameStore.getState().setCinematicPlaying(false);
|
||||||
|
dialogueOptions.onComplete?.();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ const GAME_REPAIR_ZONES = [
|
|||||||
mission: "bike",
|
mission: "bike",
|
||||||
position: [8, 0, -6],
|
position: [8, 0, -6],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
mission: "pylone",
|
|
||||||
position: [64, 0, -66],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
mission: "ferme",
|
mission: "ferme",
|
||||||
position: [-24, 0, 42],
|
position: [-24, 0, 42],
|
||||||
|
|||||||
Reference in New Issue
Block a user