From 1b7813a5bbb57008943bfa2974c678e938eb5cb0 Mon Sep 17 00:00:00 2001
From: math-pixel <59537610+math-pixel@users.noreply.github.com>
Date: Mon, 11 May 2026 11:03:01 +0200
Subject: [PATCH] update flow
---
GAME_FLOW.md | 114 +++++++++++++++++++++++
src/App.tsx | 3 +
src/components/game/GameFlow.tsx | 57 ++++++++++++
src/components/ui/IntroUI.tsx | 129 ++++++++++++++++++++++++++
src/data/audioConfig.ts | 4 +
src/hooks/useGameStep.ts | 7 +-
src/stateManager/AudioManager.ts | 40 ++++++++
src/stateManager/GameStepManager.ts | 46 ++++++++-
src/types/game.ts | 11 ++-
src/world/World.tsx | 2 +
src/world/player/PlayerController.tsx | 8 ++
11 files changed, 415 insertions(+), 6 deletions(-)
create mode 100644 GAME_FLOW.md
create mode 100644 src/components/game/GameFlow.tsx
create mode 100644 src/components/ui/IntroUI.tsx
create mode 100644 src/data/audioConfig.ts
diff --git a/GAME_FLOW.md b/GAME_FLOW.md
new file mode 100644
index 0000000..3b6cc0b
--- /dev/null
+++ b/GAME_FLOW.md
@@ -0,0 +1,114 @@
+# Game Flow - La Fabrik
+
+## Étapes du jeu
+
+```
+intro → start-intro → naming → bienvenue → star-move → outOfFabrik
+```
+
+---
+
+## Détail des étapes
+
+### 1. `intro` (initial)
+
+- État initial au chargement du jeu
+- Aucune action, juste une étape de départ
+
+### 2. `start-intro`
+
+- **Déclenchement** : Auto-transition depuis `intro` quand la scène est chargée
+- **Action** : Joue l'audio d'intro via `AudioManager.playSoundWithCallback()`
+- **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. `outOfFabrik`
+
+- **Déclenchement** : Quand le joueur entre dans la zone de sortie
+- **Action** : Transition vers l'étape finale
+- **Zone** : Détectée par `ZoneDetection` quand le joueur approche de la position configurée
+
+---
+
+## Fichiers clés
+
+| Fichier | Rôle |
+| ---------------------------------------- | ------------------------------------------------------------- |
+| `src/stateManager/GameStepManager.ts` | Gère l'état global du jeu (étape actuelle, prénom, mouvement) |
+| `src/components/game/GameFlow.tsx` | Gère les transitions automatiques et la lecture audio |
+| `src/components/ui/IntroUI.tsx` | Affiche l'input pour le prénom |
+| `src/components/ui/BienvenueDisplay.tsx` | Affiche le message de bienvenue |
+| `src/components/zone/ZoneDetection.tsx` | Détecte quand le joueur entre dans une zone |
+| `src/data/audioConfig.ts` | Chemins des fichiers audio |
+| `src/data/zones.ts` | Configuration des zones de transition |
+
+---
+
+## Configuration audio
+
+```typescript
+// src/data/audioConfig.ts
+export const AUDIO_PATHS = {
+ intro: "/sounds/fa.mp3", // Audio joué pendant start-intro
+ bienvenue: "/sounds/fa.mp3", // Audio joué pendant bienvenue
+};
+```
+
+---
+
+## Configuration des zones
+
+```typescript
+// src/data/zones.ts
+export const ZONES: Zone[] = [
+ {
+ id: "fabrikExit",
+ position: [50, 0, 50], // Position de la zone de sortie
+ radius: 10, // Rayon de détection
+ height: 20, // Hauteur de la zone (pour la visualisation)
+ targetStep: "outOfFabrik", // Étape cible quand on entre dans la zone
+ },
+];
+```
+
+---
+
+## 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
+
+---
+
+## Notes techniques
+
+- Le mouvement du joueur est bloqué tant que `canMove` est `false`
+- `useSyncExternalStore` est utilisé pour synchroniser l'état du jeu avec React
+- Les transitions sont gérées par le `GameStepManager` via le pattern singleton
+- L'audio utilise un callback `onEnded` pour déclencher les transitions automatiques
diff --git a/src/App.tsx b/src/App.tsx
index 5879bcd..0066013 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,6 +2,7 @@ import { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
import { Crosshair } from "@/components/ui/Crosshair";
import { InteractPrompt } from "@/components/ui/InteractPrompt";
+import { IntroUI, BienvenueDisplay } from "@/components/ui/IntroUI";
import { DebugPerf } from "@/utils/debug/DebugPerf";
import { World } from "@/world/World";
@@ -16,6 +17,8 @@ function App(): React.JSX.Element {
+
+
>
);
}
diff --git a/src/components/game/GameFlow.tsx b/src/components/game/GameFlow.tsx
new file mode 100644
index 0000000..597382e
--- /dev/null
+++ b/src/components/game/GameFlow.tsx
@@ -0,0 +1,57 @@
+import { useEffect, useRef, useState } from "react";
+import { GameStepManager } from "@/stateManager/GameStepManager";
+import { AudioManager } from "@/stateManager/AudioManager";
+import { AUDIO_PATHS } from "@/data/audioConfig";
+
+export function GameFlow(): null {
+ const manager = GameStepManager.getInstance();
+ const hasInitialized = useRef(false);
+ const [step, setStep] = useState(manager.getStep());
+
+ useEffect(() => {
+ const unsubscribe = manager.subscribe(() => {
+ setStep(manager.getStep());
+ });
+ return unsubscribe;
+ }, [manager]);
+
+ useEffect(() => {
+ console.log("[GameFlow] Current step:", step);
+ if (!hasInitialized.current && step === "intro") {
+ hasInitialized.current = true;
+ console.log("[GameFlow] Transition to start-intro");
+ manager.transitionTo("start-intro");
+ }
+ }, [step, manager]);
+
+ useEffect(() => {
+ console.log("[GameFlow] useEffect triggered, step:", step);
+
+ if (step === "start-intro") {
+ console.log("[GameFlow] Playing intro audio");
+ const audio = AudioManager.getInstance();
+ audio.playSoundWithCallback(AUDIO_PATHS.intro, 0.5, () => {
+ console.log("[GameFlow] Intro audio ended, transition to naming");
+ manager.transitionTo("naming");
+ });
+
+ return () => {};
+ }
+
+ if (step === "bienvenue") {
+ console.log("[GameFlow] Playing bienvenue audio");
+ const audio = AudioManager.getInstance();
+ audio.playSoundWithCallback(AUDIO_PATHS.bienvenue, 0.5, () => {
+ console.log("[GameFlow] Bienvenue audio ended, enable movement");
+ manager.setCanMove(true);
+ manager.transitionTo("star-move");
+ });
+
+ return () => {};
+ }
+
+ return undefined;
+ }, [step, manager]);
+
+ return null;
+}
diff --git a/src/components/ui/IntroUI.tsx b/src/components/ui/IntroUI.tsx
new file mode 100644
index 0000000..10ffaad
--- /dev/null
+++ b/src/components/ui/IntroUI.tsx
@@ -0,0 +1,129 @@
+import { useState } from "react";
+import { useGameStep } from "@/hooks/useGameStep";
+
+export function IntroUI(): React.JSX.Element | null {
+ const { step, setPlayerName, transitionTo } = useGameStep();
+ const [inputValue, setInputValue] = useState("");
+
+ if (step !== "naming") return null;
+
+ const handleSubmit = (): void => {
+ if (inputValue.trim() === "") return;
+
+ console.log("[IntroUI] Submitting, name:", inputValue.trim());
+ setPlayerName(inputValue.trim());
+ console.log("[IntroUI] Calling transitionTo('bienvenue')");
+ transitionTo("bienvenue");
+ console.log("[IntroUI] After transitionTo, step should be:", step);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent): void => {
+ if (e.key === "Enter") {
+ handleSubmit();
+ }
+ };
+
+ return (
+
+
+
+ Quel est votre prénom ?
+
+ setInputValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder="Votre prénom"
+ autoFocus
+ style={{
+ padding: "0.75rem",
+ fontSize: "1rem",
+ borderRadius: "6px",
+ border: "1px solid #444",
+ backgroundColor: "#2a2a2a",
+ color: "#fff",
+ outline: "none",
+ }}
+ />
+
+
+
+ );
+}
+
+export function BienvenueDisplay(): React.JSX.Element | null {
+ const { step, playerName } = useGameStep();
+
+ if (step !== "bienvenue") return null;
+
+ return (
+
+
+ Bienvenue {playerName} !
+
+
+ );
+}
diff --git a/src/data/audioConfig.ts b/src/data/audioConfig.ts
new file mode 100644
index 0000000..6a6edd1
--- /dev/null
+++ b/src/data/audioConfig.ts
@@ -0,0 +1,4 @@
+export const AUDIO_PATHS = {
+ intro: "/sounds/fa.mp3",
+ bienvenue: "/sounds/fa.mp3",
+} as const;
diff --git a/src/hooks/useGameStep.ts b/src/hooks/useGameStep.ts
index 03d07ef..a956f1e 100644
--- a/src/hooks/useGameStep.ts
+++ b/src/hooks/useGameStep.ts
@@ -5,8 +5,7 @@ import type { GameStepSnapshot } from "@/types/game";
const manager = GameStepManager.getInstance();
export function useGameStep(): GameStepSnapshot {
- return useSyncExternalStore(manager.subscribe.bind(manager), () => ({
- step: manager.getStep(),
- transitionTo: manager.transitionTo.bind(manager),
- }));
+ return useSyncExternalStore(manager.subscribe.bind(manager), () =>
+ manager.getSnapshot(),
+ );
}
diff --git a/src/stateManager/AudioManager.ts b/src/stateManager/AudioManager.ts
index 1fcc256..4e59ca5 100644
--- a/src/stateManager/AudioManager.ts
+++ b/src/stateManager/AudioManager.ts
@@ -40,6 +40,46 @@ export class AudioManager {
});
}
+ playSoundWithCallback(
+ path: string,
+ volume: number,
+ onEnded: () => void,
+ ): void {
+ console.log("[AudioManager] playSoundWithCallback:", path);
+ const audio = new Audio(path);
+ audio.volume = Math.max(0, Math.min(1, volume));
+ audio.currentTime = 0;
+
+ audio.addEventListener("ended", () => {
+ console.log("[AudioManager] Audio ended:", path);
+ onEnded();
+ });
+
+ audio.addEventListener("error", (e) => {
+ console.error("[AudioManager] Audio error:", path, e);
+ });
+
+ audio
+ .play()
+ .then(() => {
+ console.log("[AudioManager] Audio playing:", path);
+ })
+ .catch((error: unknown) => {
+ console.error("[AudioManager] Play failed:", path, error);
+ if (
+ error instanceof DOMException &&
+ AudioManager.IGNORED_PLAYBACK_ERRORS.has(error.name)
+ ) {
+ return;
+ }
+
+ logger.error("AudioManager", "Failed to play sound", {
+ path,
+ error: AudioManager._toLogValue(error),
+ });
+ });
+ }
+
destroy(): void {
this._audioPools.forEach((pool) => {
pool.forEach((audio) => {
diff --git a/src/stateManager/GameStepManager.ts b/src/stateManager/GameStepManager.ts
index aae5527..8d56988 100644
--- a/src/stateManager/GameStepManager.ts
+++ b/src/stateManager/GameStepManager.ts
@@ -1,10 +1,13 @@
-import type { GameStep } from "@/types/game";
+import type { GameStep, GameStepSnapshot } from "@/types/game";
export class GameStepManager {
private static _instance: GameStepManager | null = null;
private _currentStep: GameStep = "intro";
+ private _playerName = "";
+ private _canMove = false;
private readonly _listeners = new Set<() => void>();
+ private _cachedSnapshot: GameStepSnapshot | null = null;
static getInstance(): GameStepManager {
if (!GameStepManager._instance) {
@@ -20,10 +23,48 @@ export class GameStepManager {
return this._currentStep;
}
+ getPlayerName(): string {
+ return this._playerName;
+ }
+
+ canMove(): boolean {
+ return this._canMove;
+ }
+
+ getSnapshot(): GameStepSnapshot {
+ if (!this._cachedSnapshot) {
+ this._cachedSnapshot = {
+ step: this._currentStep,
+ playerName: this._playerName,
+ canMove: this._canMove,
+ transitionTo: this.transitionTo.bind(this),
+ setPlayerName: this.setPlayerName.bind(this),
+ };
+ }
+ return this._cachedSnapshot;
+ }
+
transitionTo(step: GameStep): void {
if (this._currentStep === step) return;
this._currentStep = step;
+ this._cachedSnapshot = null;
+ this._emit();
+ }
+
+ setPlayerName(name: string): void {
+ if (this._playerName === name) return;
+
+ this._playerName = name;
+ this._cachedSnapshot = null;
+ this._emit();
+ }
+
+ setCanMove(canMove: boolean): void {
+ if (this._canMove === canMove) return;
+
+ this._canMove = canMove;
+ this._cachedSnapshot = null;
this._emit();
}
@@ -37,7 +78,10 @@ export class GameStepManager {
destroy(): void {
this._currentStep = "intro";
+ this._playerName = "";
+ this._canMove = false;
this._listeners.clear();
+ this._cachedSnapshot = null;
GameStepManager._instance = null;
}
diff --git a/src/types/game.ts b/src/types/game.ts
index 5f79fe5..5f5e397 100644
--- a/src/types/game.ts
+++ b/src/types/game.ts
@@ -1,6 +1,12 @@
import type { Vector3Tuple } from "@/types/3d";
-export type GameStep = "intro" | "outOfFabrik";
+export type GameStep =
+ | "intro"
+ | "start-intro"
+ | "naming"
+ | "bienvenue"
+ | "star-move"
+ | "outOfFabrik";
export interface Zone {
id: string;
@@ -16,5 +22,8 @@ export interface GameState {
export interface GameStepSnapshot {
step: GameStep;
+ playerName: string;
+ canMove: boolean;
transitionTo: (step: GameStep) => void;
+ setPlayerName: (name: string) => void;
}
diff --git a/src/world/World.tsx b/src/world/World.tsx
index ab24ce6..798af29 100644
--- a/src/world/World.tsx
+++ b/src/world/World.tsx
@@ -10,6 +10,7 @@ import {
ZoneDebugVisuals,
ZoneDetection,
} from "@/components/zone/ZoneDetection";
+import { GameFlow } from "@/components/game/GameFlow";
import { DebugCameraControls } from "@/utils/debug/scene/DebugCameraControls";
import { DebugHelpers } from "@/utils/debug/scene/DebugHelpers";
import { Environment } from "@/world/Environment";
@@ -34,6 +35,7 @@ export function World(): React.JSX.Element {
+
{cameraMode === "debug" ? : null}
{sceneMode === "game" ? (
diff --git a/src/world/player/PlayerController.tsx b/src/world/player/PlayerController.tsx
index 341c6d9..50ff41d 100644
--- a/src/world/player/PlayerController.tsx
+++ b/src/world/player/PlayerController.tsx
@@ -24,6 +24,7 @@ import {
PLAYER_XZ_DAMPING_FACTOR,
} from "@/data/playerConfig";
import { InteractionManager } from "@/stateManager/InteractionManager";
+import { GameStepManager } from "@/stateManager/GameStepManager";
import type { Vector3Tuple } from "@/types/3d";
type Keys = {
@@ -63,6 +64,7 @@ export function PlayerController({
const velocity = useRef(new THREE.Vector3());
const onFloor = useRef(false);
const wantsJump = useRef(false);
+ const gameStepManager = GameStepManager.getInstance();
const capsule = useRef(
new Capsule(
@@ -165,6 +167,12 @@ export function PlayerController({
}, []);
useFrame((_, delta) => {
+ if (!gameStepManager.canMove()) {
+ velocity.current.set(0, 0, 0);
+ camera.position.copy(capsule.current.end);
+ return;
+ }
+
const dt = Math.min(delta, PLAYER_MAX_DELTA);
camera.getWorldDirection(_forward);