From 118e5f3b4a793793b4cf116b7a0f6a8a49408824 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Fri, 8 May 2026 01:09:42 +0100 Subject: [PATCH] update: add generic repair mission store helpers --- .agent/AGENT.md | 2 +- docs/technical/zustand.md | 12 +++ src/data/docs/docsTranslations.ts | 12 +++ src/managers/stores/useGameStore.ts | 123 ++++++++++++++++------------ 4 files changed, 95 insertions(+), 54 deletions(-) diff --git a/.agent/AGENT.md b/.agent/AGENT.md index aa8477b..deaf3c4 100644 --- a/.agent/AGENT.md +++ b/.agent/AGENT.md @@ -11,7 +11,7 @@ You are working on **La Fabrik**, an interactive 3D web experience built with Re ## Current Implementation - Stack: React 19, Three.js, `@react-three/fiber`, `@react-three/drei`, `@react-three/rapier`, TypeScript, Vite -- No external global state library is used. +- Zustand is used for shared game progression state. - Current singleton-style services are limited to: - `InteractionManager` - `AudioManager` diff --git a/docs/technical/zustand.md b/docs/technical/zustand.md index 80b6f5c..27e0f9a 100644 --- a/docs/technical/zustand.md +++ b/docs/technical/zustand.md @@ -114,6 +114,18 @@ setMainState("bike"); Direct setters are useful for debug panels, but production gameplay should prefer business actions such as `advanceGameState`, `completeBike`, or `completePylone`. +Mission gameplay that can target `bike`, `pylone`, or `ferme` should prefer the generic mission actions: + +```ts +const setMissionStep = useGameStore((state) => state.setMissionStep); +const completeMission = useGameStore((state) => state.completeMission); + +setMissionStep("bike", "inspected"); +completeMission("bike"); +``` + +This keeps reusable gameplay components such as repair flows from duplicating mission-specific branches like `setBikeState`, `setPyloneState`, and `setFermeState`. + ## World Integration `src/world/GameStageContent.tsx` subscribes to `mainState` and mounts stage-specific content. diff --git a/src/data/docs/docsTranslations.ts b/src/data/docs/docsTranslations.ts index 166cfc0..9eac4ed 100644 --- a/src/data/docs/docsTranslations.ts +++ b/src/data/docs/docsTranslations.ts @@ -321,6 +321,18 @@ setMainState("bike"); Les setters directs sont pratiques pour les panneaux debug, mais le gameplay de production devrait préférer les actions métier comme \`advanceGameState\`, \`completeBike\` ou \`completePylone\`. +Le gameplay de mission qui peut cibler \`bike\`, \`pylone\` ou \`ferme\` doit préférer les actions génériques de mission : + +\`\`\`ts +const setMissionStep = useGameStore((state) => state.setMissionStep); +const completeMission = useGameStore((state) => state.completeMission); + +setMissionStep("bike", "inspected"); +completeMission("bike"); +\`\`\` + +Cela évite aux composants gameplay réutilisables, comme les flows de réparation, de dupliquer des branches spécifiques à chaque mission avec \`setBikeState\`, \`setPyloneState\` et \`setFermeState\`. + ## Intégration avec le World \`src/world/GameStageContent.tsx\` s'abonne à \`mainState\` et monte le contenu spécifique au state courant. diff --git a/src/managers/stores/useGameStore.ts b/src/managers/stores/useGameStore.ts index c58bc89..d49865e 100644 --- a/src/managers/stores/useGameStore.ts +++ b/src/managers/stores/useGameStore.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro"; +export type RepairMissionId = "bike" | "pylone" | "ferme"; export type MissionStep = | "locked" | "waiting" @@ -46,10 +47,12 @@ interface GameActions { setPyloneState: (pylone: Partial) => void; setFermeState: (ferme: Partial) => void; setOutroState: (outro: Partial) => void; + setMissionStep: (mission: RepairMissionId, step: MissionStep) => void; completeIntro: () => void; completeBike: () => void; completePylone: () => void; completeFerme: () => void; + completeMission: (mission: RepairMissionId) => void; startOutro: () => void; advanceGameState: () => void; rewindGameState: () => void; @@ -59,6 +62,12 @@ interface GameActions { type GameStore = GameState & GameActions; type GameStateUpdate = Partial; +export const REPAIR_MISSION_IDS = ["bike", "pylone", "ferme"] as const; + +function isRepairMissionId(value: MainGameState): value is RepairMissionId { + return REPAIR_MISSION_IDS.includes(value as RepairMissionId); +} + function getNextMissionStep(step: MissionStep): MissionStep { switch (step) { case "locked": @@ -155,6 +164,56 @@ function completeFermeState(state: GameState): GameStateUpdate { }; } +function setMissionStepState( + state: GameState, + mission: RepairMissionId, + step: MissionStep, +): GameStateUpdate { + return { + [mission]: { + ...state[mission], + currentStep: step, + }, + }; +} + +function completeMissionState( + state: GameState, + mission: RepairMissionId, +): GameStateUpdate { + switch (mission) { + case "bike": + return completeBikeState(state); + case "pylone": + return completePyloneState(state); + case "ferme": + return completeFermeState(state); + } +} + +function advanceRepairMissionState( + state: GameState, + mission: RepairMissionId, +): GameStateUpdate { + const nextStep = getNextMissionStep(state[mission].currentStep); + if (nextStep === "done") { + return completeMissionState(state, mission); + } + + return setMissionStepState(state, mission, nextStep); +} + +function rewindRepairMissionState( + state: GameState, + mission: RepairMissionId, +): GameStateUpdate { + return setMissionStepState( + state, + mission, + getPreviousMissionStep(state[mission].currentStep), + ); +} + function startOutroState(state: GameState): GameStateUpdate { return { mainState: "outro", @@ -208,10 +267,14 @@ export const useGameStore = create()((set) => ({ set((state) => ({ ferme: { ...state.ferme, ...ferme } })), setOutroState: (outro) => set((state) => ({ outro: { ...state.outro, ...outro } })), + setMissionStep: (mission, step) => + set((state) => setMissionStepState(state, mission, step)), completeIntro: () => set(completeIntroState), - completeBike: () => set(completeBikeState), - completePylone: () => set(completePyloneState), - completeFerme: () => set(completeFermeState), + 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)), startOutro: () => set(startOutroState), advanceGameState: () => set((state) => { @@ -219,31 +282,8 @@ export const useGameStore = create()((set) => ({ return completeIntroState(state); } - if (state.mainState === "bike") { - const nextStep = getNextMissionStep(state.bike.currentStep); - if (nextStep === "done") { - return completeBikeState(state); - } - - return { bike: { ...state.bike, currentStep: nextStep } }; - } - - if (state.mainState === "pylone") { - const nextStep = getNextMissionStep(state.pylone.currentStep); - if (nextStep === "done") { - return completePyloneState(state); - } - - return { pylone: { ...state.pylone, currentStep: nextStep } }; - } - - if (state.mainState === "ferme") { - const nextStep = getNextMissionStep(state.ferme.currentStep); - if (nextStep === "done") { - return completeFermeState(state); - } - - return { ferme: { ...state.ferme, currentStep: nextStep } }; + if (isRepairMissionId(state.mainState)) { + return advanceRepairMissionState(state, state.mainState); } return startOutroState(state); @@ -254,31 +294,8 @@ export const useGameStore = create()((set) => ({ return { intro: { ...state.intro, hasCompleted: false } }; } - if (state.mainState === "bike") { - return { - bike: { - ...state.bike, - currentStep: getPreviousMissionStep(state.bike.currentStep), - }, - }; - } - - if (state.mainState === "pylone") { - return { - pylone: { - ...state.pylone, - currentStep: getPreviousMissionStep(state.pylone.currentStep), - }, - }; - } - - if (state.mainState === "ferme") { - return { - ferme: { - ...state.ferme, - currentStep: getPreviousMissionStep(state.ferme.currentStep), - }, - }; + if (isRepairMissionId(state.mainState)) { + return rewindRepairMissionState(state, state.mainState); } return { outro: { ...state.outro, hasStarted: false } };