From 4c5e2ed945e5b5bd6ba437f3fbeb4e4c56bc96fb Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Sat, 30 May 2026 02:14:10 +0200 Subject: [PATCH] feat(types): add SiteStep and refactor GameStep for new intro flow --- src/components/game/GameFlow.tsx | 72 --------- .../three/interaction/NPCHelper.tsx | 42 ----- .../three/interaction/PyloneDestroyed.tsx | 52 ------ src/components/ui/IntroUI.tsx | 129 --------------- src/components/zone/ZoneDetection.tsx | 148 ------------------ src/data/game/gameStateConfig.ts | 35 +++-- src/data/zones.ts | 19 --- src/managers/stores/useGameStore.ts | 2 +- src/pages/page.tsx | 3 - src/types/game.ts | 36 ++--- src/world/World.tsx | 12 -- 11 files changed, 42 insertions(+), 508 deletions(-) delete mode 100644 src/components/game/GameFlow.tsx delete mode 100644 src/components/three/interaction/NPCHelper.tsx delete mode 100644 src/components/three/interaction/PyloneDestroyed.tsx delete mode 100644 src/components/ui/IntroUI.tsx delete mode 100644 src/components/zone/ZoneDetection.tsx delete mode 100644 src/data/zones.ts diff --git a/src/components/game/GameFlow.tsx b/src/components/game/GameFlow.tsx deleted file mode 100644 index 2720a22..0000000 --- a/src/components/game/GameFlow.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useEffect, useRef } from "react"; -import { AudioManager } from "@/managers/AudioManager"; -import { useGameStore } from "@/managers/stores/useGameStore"; -import { AUDIO_PATHS } from "@/data/audioConfig"; - -export function GameFlow(): null { - const step = useGameStore((state) => state.intro.currentStep); - const setStep = useGameStore((state) => state.setIntroStep); - const setActivityCity = useGameStore((state) => state.setActivityCity); - const setCanMove = useGameStore((state) => state.setCanMove); - const completeIntro = useGameStore((state) => state.completeIntro); - const hasInitialized = useRef(false); - - useEffect(() => { - if (!hasInitialized.current && step === "intro") { - hasInitialized.current = true; - setStep("start-intro"); - } - }, [step, setStep]); - - useEffect(() => { - if (step === "start-intro") { - const audio = AudioManager.getInstance(); - audio.playSoundWithCallback(AUDIO_PATHS.intro, 0.5, () => { - setStep("naming"); - }); - - return () => {}; - } - - if (step === "bienvenue") { - const audio = AudioManager.getInstance(); - audio.playSoundWithCallback(AUDIO_PATHS.bienvenue, 0.5, () => { - setCanMove(true); - setStep("star-move"); - }); - - return () => {}; - } - - if (step === "mission2") { - setActivityCity(false); - const audio = AudioManager.getInstance(); - audio.playSound(AUDIO_PATHS.alertCentral, 0.5); - } - - if (step === "searching") { - const audio = AudioManager.getInstance(); - audio.playSound(AUDIO_PATHS.searching, 0.5); - } - - if (step === "helped") { - const audio = AudioManager.getInstance(); - audio.playSound(AUDIO_PATHS.helped, 0.5); - } - - if (step === "manipulation") { - setCanMove(false); - const timeoutId = window.setTimeout(() => { - completeIntro(); - }, 1000); - - return () => { - window.clearTimeout(timeoutId); - }; - } - - return undefined; - }, [completeIntro, step, setStep, setActivityCity, setCanMove]); - - return null; -} diff --git a/src/components/three/interaction/NPCHelper.tsx b/src/components/three/interaction/NPCHelper.tsx deleted file mode 100644 index e89e981..0000000 --- a/src/components/three/interaction/NPCHelper.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { InteractableObject } from "@/components/three/interaction/InteractableObject"; -import { useGameStore } from "@/managers/stores/useGameStore"; -import { Debug } from "@/utils/debug/Debug"; -import type { Vector3Tuple } from "@/types/three/three"; - -interface NPCHelperProps { - position: Vector3Tuple; -} - -export function NPCHelper({ position }: NPCHelperProps): React.JSX.Element { - const step = useGameStore((state) => state.intro.currentStep); - const setStep = useGameStore((state) => state.setIntroStep); - const debug = Debug.getInstance(); - - const handlePress = (): void => { - if (step === "searching") { - setStep("helped"); - } - }; - - const shouldShow = step === "searching" || debug.active; - - if (!shouldShow) { - return <>; - } - - return ( - - - - - - - - - ); -} diff --git a/src/components/three/interaction/PyloneDestroyed.tsx b/src/components/three/interaction/PyloneDestroyed.tsx deleted file mode 100644 index f7c567c..0000000 --- a/src/components/three/interaction/PyloneDestroyed.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { InteractableObject } from "@/components/three/interaction/InteractableObject"; -import { useGameStore } from "@/managers/stores/useGameStore"; -import { Debug } from "@/utils/debug/Debug"; -import type { Vector3Tuple } from "@/types/three/three"; - -interface PyloneDestroyedProps { - position: Vector3Tuple; -} - -export function PyloneDestroyed({ - position, -}: PyloneDestroyedProps): React.JSX.Element { - const step = useGameStore((state) => state.intro.currentStep); - const setStep = useGameStore((state) => state.setIntroStep); - const setCanMove = useGameStore((state) => state.setCanMove); - const showDialog = useGameStore((state) => state.showDialog); - const debug = Debug.getInstance(); - - const handlePress = (): void => { - if (step === "helped") { - setCanMove(false); - setStep("manipulation"); - } else if (step === "searching") { - showDialog( - "Cet objet est trop lourd pour le porter tout seul, trouve de l'aide", - ); - } - }; - - const shouldShow = - step === "helped" || step === "manipulation" || debug.active; - - if (!shouldShow) { - return <>; - } - - return ( - - - - - - - - - ); -} diff --git a/src/components/ui/IntroUI.tsx b/src/components/ui/IntroUI.tsx deleted file mode 100644 index 45ace4e..0000000 --- a/src/components/ui/IntroUI.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { useState } from "react"; -import { useGameStore } from "@/managers/stores/useGameStore"; - -export function IntroUI(): React.JSX.Element | null { - const step = useGameStore((state) => state.intro.currentStep); - const setPlayerName = useGameStore((state) => state.setPlayerName); - const setStep = useGameStore((state) => state.setIntroStep); - const [inputValue, setInputValue] = useState(""); - - if (step !== "naming") return null; - - const handleSubmit = (): void => { - if (inputValue.trim() === "") return; - - setPlayerName(inputValue.trim()); - setStep("bienvenue"); - }; - - const handleKeyDown = (e: React.KeyboardEvent): void => { - if (e.key === "Enter") { - handleSubmit(); - } - }; - - return ( -
-
-

- Quel est votre prenom ? -

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

- Bienvenue {playerName} ! -

-
- ); -} diff --git a/src/components/zone/ZoneDetection.tsx b/src/components/zone/ZoneDetection.tsx deleted file mode 100644 index 0bbb1c1..0000000 --- a/src/components/zone/ZoneDetection.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { useFrame, useThree } from "@react-three/fiber"; -import * as THREE from "three"; -import { ZONES } from "@/data/zones"; -import { useGameStore } from "@/managers/stores/useGameStore"; -import { Debug } from "@/utils/debug/Debug"; -import { GAME_STEPS } from "@/data/game/gameStateConfig"; - -const _playerPos = new THREE.Vector3(); -const _zonePos = new THREE.Vector3(); - -export function ZoneDetection(): null { - const camera = useThree((state) => state.camera); - const triggeredZones = useRef>(new Set()); - const debug = Debug.getInstance(); - const step = useGameStore((state) => state.intro.currentStep); - const setStep = useGameStore((state) => state.setIntroStep); - - useEffect(() => { - if (!debug.active) return; - - const folder = debug.createFolder("Game"); - if (!folder) return; - - const gameState = { step: step }; - const playerPos = { x: 0, y: 0, z: 0 }; - - folder.add(gameState, "step", GAME_STEPS).name("Game Step").disable(); - - folder.add(playerPos, "x").name("Player X").listen().disable(); - folder.add(playerPos, "y").name("Player Y").listen().disable(); - folder.add(playerPos, "z").name("Player Z").listen().disable(); - - const unsubStore = useGameStore.subscribe((state) => { - gameState.step = state.intro.currentStep; - folder.controllersRecursive().forEach((c) => c.updateDisplay()); - }); - - let frameId: number; - const updatePlayerPos = (): void => { - camera.getWorldPosition(_playerPos); - playerPos.x = Math.round(_playerPos.x * 100) / 100; - playerPos.y = Math.round(_playerPos.y * 100) / 100; - playerPos.z = Math.round(_playerPos.z * 100) / 100; - folder.controllersRecursive().forEach((c) => c.updateDisplay()); - frameId = requestAnimationFrame(updatePlayerPos); - }; - updatePlayerPos(); - - return () => { - cancelAnimationFrame(frameId); - debug.destroyFolder("Game"); - unsubStore(); - }; - }, [debug, camera, step]); - - useFrame(() => { - camera.getWorldPosition(_playerPos); - - for (const zone of ZONES) { - if (triggeredZones.current.has(zone.id)) continue; - - _zonePos.set(...zone.position); - - const distanceSq = _playerPos.distanceToSquared(_zonePos); - - if (distanceSq <= zone.radius * zone.radius) { - setStep(zone.targetStep); - triggeredZones.current.add(zone.id); - break; - } - } - }); - - return null; -} - -export function ZoneDebugVisuals(): React.JSX.Element | null { - const debug = Debug.getInstance(); - const camera = useThree((state) => state.camera); - const [triggeredZones, setTriggeredZones] = useState>(new Set()); - - useFrame(() => { - camera.getWorldPosition(_playerPos); - - for (const zone of ZONES) { - if (triggeredZones.has(zone.id)) continue; - - _zonePos.set(...zone.position); - - const distanceSq = _playerPos.distanceToSquared(_zonePos); - - if (distanceSq <= zone.radius * zone.radius) { - setTriggeredZones((prev) => new Set(prev).add(zone.id)); - break; - } - } - }); - - if (!debug.active) return null; - - return ( - <> - {ZONES.map((zone) => ( - - ))} - - ); -} - -function ZoneVisual({ - position, - radius, - height, - triggered, -}: { - position: [number, number, number]; - radius: number; - height: number; - triggered: boolean; -}): React.JSX.Element { - const color = triggered ? "#00ff00" : "#ff0000"; - - return ( - - - - - - - - - - - ); -} diff --git a/src/data/game/gameStateConfig.ts b/src/data/game/gameStateConfig.ts index d85af1a..be1b286 100644 --- a/src/data/game/gameStateConfig.ts +++ b/src/data/game/gameStateConfig.ts @@ -1,16 +1,24 @@ -import type { GameStep, MainGameState } from "@/types/game"; +import type { GameStep, MainGameState, SiteStep } from "@/types/game"; -export const GAME_STEPS: readonly GameStep[] = [ - "intro", - "start-intro", +/** + * Steps for the /site onboarding page + */ +export const SITE_STEPS: readonly SiteStep[] = [ + "welcome", + "situation", "naming", - "bienvenue", - "star-move", - "mission2", - "searching", - "helped", - "manipulation", - "outOfFabrik", + "transition", +]; + +/** + * Steps for the intro sequence (after /site, on / route) + */ +export const GAME_STEPS: readonly GameStep[] = [ + "loading-map", + "video", + "dialogue-intro", + "reveal", + "playing", ]; export const MAIN_GAME_STATES: readonly MainGameState[] = [ @@ -21,9 +29,14 @@ export const MAIN_GAME_STATES: readonly MainGameState[] = [ "outro", ] as const; +const SITE_STEP_VALUES: ReadonlySet = new Set(SITE_STEPS); const GAME_STEP_VALUES: ReadonlySet = new Set(GAME_STEPS); const MAIN_GAME_STATE_VALUES: ReadonlySet = new Set(MAIN_GAME_STATES); +export function isSiteStep(value: unknown): value is SiteStep { + return typeof value === "string" && SITE_STEP_VALUES.has(value); +} + export function isGameStep(value: unknown): value is GameStep { return typeof value === "string" && GAME_STEP_VALUES.has(value); } diff --git a/src/data/zones.ts b/src/data/zones.ts deleted file mode 100644 index cf88747..0000000 --- a/src/data/zones.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Zone } from "@/types/game"; -import type { Vector3Tuple } from "@/types/three/three"; - -export const ZONES: Zone[] = [ - { - id: "fabrikExit", - position: [-5, 25, -15] as Vector3Tuple, - radius: 10, - height: 20, - targetStep: "mission2", - }, - { - id: "searchingZone", - position: [-5, 25, -30] as Vector3Tuple, - radius: 10, - height: 20, - targetStep: "searching", - }, -]; diff --git a/src/managers/stores/useGameStore.ts b/src/managers/stores/useGameStore.ts index 099caab..e1eb7e5 100644 --- a/src/managers/stores/useGameStore.ts +++ b/src/managers/stores/useGameStore.ts @@ -255,7 +255,7 @@ function createInitialGameState(): GameState { currentSpeed: PLAYER_WALK_SPEED, }, intro: { - currentStep: "intro", + currentStep: "loading-map", dialogueAudio: null, hasCompleted: false, isEbikeUnlocked: false, diff --git a/src/pages/page.tsx b/src/pages/page.tsx index deae3e3..a148d3c 100644 --- a/src/pages/page.tsx +++ b/src/pages/page.tsx @@ -4,7 +4,6 @@ import * as THREE from "three"; import { DebugPerf } from "@/components/debug/DebugPerf"; import { DialogMessage } from "@/components/ui/DialogMessage"; import { GameUI } from "@/components/ui/GameUI"; -import { BienvenueDisplay, IntroUI } from "@/components/ui/IntroUI"; import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay"; import { INITIAL_SCENE_LOADING_STATE } from "@/data/world/sceneLoadingConfig"; import { useGameStore } from "@/managers/stores/useGameStore"; @@ -94,8 +93,6 @@ export function HomePage(): React.JSX.Element { - - {dialogMessage ? ( : null} {sceneMode === "game" ? ( <> - - - - -