From 28c6ef199f0be603e4ef7d78cca2cf9230a9e84a Mon Sep 17 00:00:00 2001 From: math-pixel <59537610+math-pixel@users.noreply.github.com> Date: Tue, 12 May 2026 21:44:43 +0200 Subject: [PATCH] feat: sequencing --- src/components/game/GameFlow.tsx | 37 ++-------- .../three/interaction/NPCHelper.tsx | 8 +-- .../three/interaction/PyloneDestroyed.tsx | 6 +- src/components/ui/IntroUI.tsx | 4 +- .../ui/debug/GameStateDebugPanel.tsx | 24 +++---- src/components/zone/ZoneDetection.tsx | 9 ++- src/data/gameplay/repairMissions.ts | 47 +------------ src/data/zones.ts | 11 +-- src/hooks/gameplay/useRepairMovementLocked.ts | 4 +- src/managers/stores/useGameStore.ts | 69 +++++++++++++------ src/types/game.ts | 24 ++----- src/types/gameplay/pylone.ts | 18 +++++ src/types/gameplay/repairMission.ts | 4 +- 13 files changed, 110 insertions(+), 155 deletions(-) create mode 100644 src/types/gameplay/pylone.ts diff --git a/src/components/game/GameFlow.tsx b/src/components/game/GameFlow.tsx index 2c06372..358a952 100644 --- a/src/components/game/GameFlow.tsx +++ b/src/components/game/GameFlow.tsx @@ -7,56 +7,29 @@ export function GameFlow(): null { const step = useGameStore((state) => state.intro.currentStep); const setStep = useGameStore((state) => state.setIntroStep); const isCinematicPlaying = useGameStore((state) => state.isCinematicPlaying); - const setActivityCity = useGameStore((state) => state.setActivityCity); const setCanMove = useGameStore((state) => state.setCanMove); const hasInitialized = useRef(false); useEffect(() => { if (!hasInitialized.current && step === "intro") { hasInitialized.current = true; - setStep("intro_sequence"); + setStep("sequence_video"); } }, [step, setStep]); useEffect(() => { - if (step === "intro_sequence" && !isCinematicPlaying) { + if (step === "sequence_video" && !isCinematicPlaying) { setStep("naming"); } }, [step, isCinematicPlaying, setStep]); useEffect(() => { - 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); + if (step === "start-move") { + setCanMove(true); } return undefined; - }, [step, setStep, setActivityCity, setCanMove]); + }, [step, setCanMove]); return null; } diff --git a/src/components/three/interaction/NPCHelper.tsx b/src/components/three/interaction/NPCHelper.tsx index e89e981..fa9806a 100644 --- a/src/components/three/interaction/NPCHelper.tsx +++ b/src/components/three/interaction/NPCHelper.tsx @@ -8,17 +8,17 @@ interface NPCHelperProps { } export function NPCHelper({ position }: NPCHelperProps): React.JSX.Element { - const step = useGameStore((state) => state.intro.currentStep); - const setStep = useGameStore((state) => state.setIntroStep); + const step = useGameStore((state) => state.pylone.currentStep); + const setPyloneStep = useGameStore((state) => state.setPyloneState); const debug = Debug.getInstance(); const handlePress = (): void => { if (step === "searching") { - setStep("helped"); + setPyloneStep({ currentStep: "helped" }); } }; - const shouldShow = step === "searching" || debug.active; + const shouldShow = step === "searching" || step === "helped" || debug.active; if (!shouldShow) { return <>; diff --git a/src/components/three/interaction/PyloneDestroyed.tsx b/src/components/three/interaction/PyloneDestroyed.tsx index f7c567c..09036e3 100644 --- a/src/components/three/interaction/PyloneDestroyed.tsx +++ b/src/components/three/interaction/PyloneDestroyed.tsx @@ -10,8 +10,8 @@ interface PyloneDestroyedProps { export function PyloneDestroyed({ position, }: PyloneDestroyedProps): React.JSX.Element { - const step = useGameStore((state) => state.intro.currentStep); - const setStep = useGameStore((state) => state.setIntroStep); + const step = useGameStore((state) => state.pylone.currentStep); + const setPyloneStep = useGameStore((state) => state.setPyloneState); const setCanMove = useGameStore((state) => state.setCanMove); const showDialog = useGameStore((state) => state.showDialog); const debug = Debug.getInstance(); @@ -19,7 +19,7 @@ export function PyloneDestroyed({ const handlePress = (): void => { if (step === "helped") { setCanMove(false); - setStep("manipulation"); + setPyloneStep({ currentStep: "manipulation" }); } else if (step === "searching") { showDialog( "Cet objet est trop lourd pour le porter tout seul, trouve de l'aide", diff --git a/src/components/ui/IntroUI.tsx b/src/components/ui/IntroUI.tsx index 45ace4e..5688aed 100644 --- a/src/components/ui/IntroUI.tsx +++ b/src/components/ui/IntroUI.tsx @@ -13,7 +13,7 @@ export function IntroUI(): React.JSX.Element | null { if (inputValue.trim() === "") return; setPlayerName(inputValue.trim()); - setStep("bienvenue"); + setStep("start-move"); }; 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 playerName = useGameStore((state) => state.missionFlow.playerName); - if (step !== "bienvenue") return null; + if (step !== "start-move") return null; return (
>(new Set()); const debug = Debug.getInstance(); const step = useGameStore((state) => state.intro.currentStep); + const mainState = useGameStore((state) => state.mainState); const setStep = useGameStore((state) => state.setIntroStep); + const setPyloneStep = useGameStore((state) => state.setPyloneState); + const advanceGameState = useGameStore((state) => state.advanceGameState); useEffect(() => { if (!debug.active) return; @@ -65,7 +68,11 @@ export function ZoneDetection(): null { const distanceSq = _playerPos.distanceToSquared(_zonePos); if (distanceSq <= zone.radius * zone.radius) { - setStep(zone.targetStep); + if (zone.targetStep === "bike" && mainState === "intro") { + advanceGameState(); + } else { + setStep(zone.targetStep); + } triggeredZones.current.add(zone.id); break; } diff --git a/src/data/gameplay/repairMissions.ts b/src/data/gameplay/repairMissions.ts index 78654d6..04d2f96 100644 --- a/src/data/gameplay/repairMissions.ts +++ b/src/data/gameplay/repairMissions.ts @@ -52,7 +52,7 @@ export const REPAIR_MISSIONS: Record = { description: "Repair the damaged cooling module before relaunching the bike", modelPath: "/models/ebike/model.gltf", - modelScale: 0.50, + modelScale: 0.5, stageUiPath: "/assets/UI/ebike.webm", interactUiPath: REPAIR_INTERACT_UI_PATH, brokenUiPath: REPAIR_BROKEN_UI_PATH, @@ -85,51 +85,6 @@ export const REPAIR_MISSIONS: Record = { }, ], }, - 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: { id: "ferme", label: "Vertical farm", diff --git a/src/data/zones.ts b/src/data/zones.ts index cf88747..ec5c5f5 100644 --- a/src/data/zones.ts +++ b/src/data/zones.ts @@ -1,4 +1,4 @@ -import type { Zone } from "@/types/game"; +import type { Zone, GameStep } from "@/types/game"; import type { Vector3Tuple } from "@/types/three/three"; export const ZONES: Zone[] = [ @@ -7,13 +7,6 @@ export const ZONES: Zone[] = [ 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", + targetStep: "bike" as GameStep, }, ]; diff --git a/src/hooks/gameplay/useRepairMovementLocked.ts b/src/hooks/gameplay/useRepairMovementLocked.ts index c92e611..188a353 100644 --- a/src/hooks/gameplay/useRepairMovementLocked.ts +++ b/src/hooks/gameplay/useRepairMovementLocked.ts @@ -2,14 +2,12 @@ import { useGameStore } from "@/managers/stores/useGameStore"; import type { MissionStep } from "@/types/gameplay/repairMission"; export function useRepairMovementLocked(): boolean { - return false; - return useGameStore((state) => { switch (state.mainState) { case "bike": return isRepairMovementLocked(state.bike.currentStep); case "pylone": - return isRepairMovementLocked(state.pylone.currentStep); + return state.pylone.currentStep === "manipulation"; case "ferme": return isRepairMovementLocked(state.ferme.currentStep); case "intro": diff --git a/src/managers/stores/useGameStore.ts b/src/managers/stores/useGameStore.ts index 15489dd..6ebfa27 100644 --- a/src/managers/stores/useGameStore.ts +++ b/src/managers/stores/useGameStore.ts @@ -7,9 +7,10 @@ import { type MissionStep, type RepairMissionId, } from "@/types/gameplay/repairMission"; +import { type PyloneStep } from "@/types/gameplay/pylone"; export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro"; -export type { MissionStep, RepairMissionId }; +export type { MissionStep, RepairMissionId, PyloneStep }; interface IntroState { currentStep: GameStep; @@ -38,7 +39,9 @@ interface GameState { bike: MissionState & { isRepaired: boolean; }; - pylone: MissionState & { + pylone: { + currentStep: PyloneStep; + dialogueAudio: string | null; isPowered: boolean; }; ferme: MissionState & { @@ -66,7 +69,6 @@ interface GameActions { setMissionStep: (mission: RepairMissionId, step: MissionStep) => void; completeIntro: () => void; completeBike: () => void; - completePylone: () => void; completeFerme: () => void; completeMission: (mission: RepairMissionId) => void; startOutro: () => void; @@ -104,22 +106,7 @@ function completeBikeState(state: GameState): GameStateUpdate { }, pylone: { ...state.pylone, - currentStep: "waiting", - }, - }; -} - -function completePyloneState(state: GameState): GameStateUpdate { - return { - mainState: "ferme", - pylone: { - ...state.pylone, - currentStep: "done", - isPowered: true, - }, - ferme: { - ...state.ferme, - currentStep: "waiting", + currentStep: "locked", }, }; } @@ -159,13 +146,42 @@ function completeMissionState( switch (mission) { case "bike": return completeBikeState(state); - case "pylone": - return completePyloneState(state); case "ferme": 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 { + 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( state: GameState, mission: RepairMissionId, @@ -273,7 +289,6 @@ export const useGameStore = create()((set) => ({ set((state) => setMissionStepState(state, mission, step)), completeIntro: () => set(completeIntroState), completeBike: () => set((state) => completeMissionState(state, "bike")), - completePylone: () => set((state) => completeMissionState(state, "pylone")), completeFerme: () => set((state) => completeMissionState(state, "ferme")), completeMission: (mission) => set((state) => completeMissionState(state, mission)), @@ -281,9 +296,19 @@ export const useGameStore = create()((set) => ({ advanceGameState: () => set((state) => { if (state.mainState === "intro") { + if (state.intro.currentStep === "bike") { + return { + mainState: "bike", + intro: { ...state.intro, hasCompleted: true }, + }; + } return completeIntroState(state); } + if (state.mainState === "pylone") { + return advancePyloneStep(state); + } + if (isRepairMissionId(state.mainState)) { return advanceRepairMissionState(state, state.mainState); } diff --git a/src/types/game.ts b/src/types/game.ts index 3b528aa..f5b9c00 100644 --- a/src/types/game.ts +++ b/src/types/game.ts @@ -2,29 +2,17 @@ import type { Vector3Tuple } from "@/types/three/three"; export type GameStep = | "intro" - | "intro_sequence" - | "start-intro" + | "sequence_video" | "naming" - | "bienvenue" - | "star-move" - | "mission2" - | "searching" - | "helped" - | "manipulation" - | "outOfFabrik"; + | "start-move" + | "bike"; export const GAME_STEPS: readonly GameStep[] = [ "intro", - "intro_sequence", - "start-intro", + "sequence_video", "naming", - "bienvenue", - "star-move", - "mission2", - "searching", - "helped", - "manipulation", - "outOfFabrik", + "start-move", + "bike", ] as const; export interface Zone { diff --git a/src/types/gameplay/pylone.ts b/src/types/gameplay/pylone.ts new file mode 100644 index 0000000..da28ea0 --- /dev/null +++ b/src/types/gameplay/pylone.ts @@ -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); +} diff --git a/src/types/gameplay/repairMission.ts b/src/types/gameplay/repairMission.ts index dc78e93..ef3a782 100644 --- a/src/types/gameplay/repairMission.ts +++ b/src/types/gameplay/repairMission.ts @@ -1,4 +1,4 @@ -export type RepairMissionId = "bike" | "pylone" | "ferme"; +export type RepairMissionId = "bike" | "ferme"; export type MissionStep = | "locked" @@ -10,7 +10,7 @@ export type MissionStep = | "reassembling" | "done"; -export const REPAIR_MISSION_IDS = ["bike", "pylone", "ferme"] as const; +export const REPAIR_MISSION_IDS = ["bike", "ferme"] as const; export const MISSION_STEPS = [ "locked",