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",