Feat/repair game #2

Merged
math-pixel merged 46 commits from feat/repair-game into develop 2026-05-11 15:33:19 +00:00
4 changed files with 95 additions and 54 deletions
Showing only changes of commit 118e5f3b4a - Show all commits
+1 -1
View File
@@ -11,7 +11,7 @@ You are working on **La Fabrik**, an interactive 3D web experience built with Re
## Current Implementation ## Current Implementation
- Stack: React 19, Three.js, `@react-three/fiber`, `@react-three/drei`, `@react-three/rapier`, TypeScript, Vite - 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: - Current singleton-style services are limited to:
- `InteractionManager` - `InteractionManager`
- `AudioManager` - `AudioManager`
+12
View File
@@ -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`. 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 ## World Integration
`src/world/GameStageContent.tsx` subscribes to `mainState` and mounts stage-specific content. `src/world/GameStageContent.tsx` subscribes to `mainState` and mounts stage-specific content.
+12
View File
@@ -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\`. 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 ## Intégration avec le World
\`src/world/GameStageContent.tsx\` s'abonne à \`mainState\` et monte le contenu spécifique au state courant. \`src/world/GameStageContent.tsx\` s'abonne à \`mainState\` et monte le contenu spécifique au state courant.
+70 -53
View File
@@ -1,6 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro"; export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro";
export type RepairMissionId = "bike" | "pylone" | "ferme";
export type MissionStep = export type MissionStep =
| "locked" | "locked"
| "waiting" | "waiting"
@@ -46,10 +47,12 @@ interface GameActions {
setPyloneState: (pylone: Partial<GameState["pylone"]>) => void; setPyloneState: (pylone: Partial<GameState["pylone"]>) => void;
setFermeState: (ferme: Partial<GameState["ferme"]>) => void; setFermeState: (ferme: Partial<GameState["ferme"]>) => void;
setOutroState: (outro: Partial<GameState["outro"]>) => void; setOutroState: (outro: Partial<GameState["outro"]>) => void;
setMissionStep: (mission: RepairMissionId, step: MissionStep) => void;
completeIntro: () => void; completeIntro: () => void;
completeBike: () => void; completeBike: () => void;
completePylone: () => void; completePylone: () => void;
completeFerme: () => void; completeFerme: () => void;
completeMission: (mission: RepairMissionId) => void;
startOutro: () => void; startOutro: () => void;
advanceGameState: () => void; advanceGameState: () => void;
rewindGameState: () => void; rewindGameState: () => void;
@@ -59,6 +62,12 @@ interface GameActions {
type GameStore = GameState & GameActions; type GameStore = GameState & GameActions;
type GameStateUpdate = Partial<GameState>; type GameStateUpdate = Partial<GameState>;
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 { function getNextMissionStep(step: MissionStep): MissionStep {
switch (step) { switch (step) {
case "locked": 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 { function startOutroState(state: GameState): GameStateUpdate {
return { return {
mainState: "outro", mainState: "outro",
@@ -208,10 +267,14 @@ export const useGameStore = create<GameStore>()((set) => ({
set((state) => ({ ferme: { ...state.ferme, ...ferme } })), set((state) => ({ ferme: { ...state.ferme, ...ferme } })),
setOutroState: (outro) => setOutroState: (outro) =>
set((state) => ({ outro: { ...state.outro, ...outro } })), set((state) => ({ outro: { ...state.outro, ...outro } })),
setMissionStep: (mission, step) =>
set((state) => setMissionStepState(state, mission, step)),
completeIntro: () => set(completeIntroState), completeIntro: () => set(completeIntroState),
completeBike: () => set(completeBikeState), completeBike: () => set((state) => completeMissionState(state, "bike")),
completePylone: () => set(completePyloneState), completePylone: () => set((state) => completeMissionState(state, "pylone")),
completeFerme: () => set(completeFermeState), completeFerme: () => set((state) => completeMissionState(state, "ferme")),
completeMission: (mission) =>
set((state) => completeMissionState(state, mission)),
startOutro: () => set(startOutroState), startOutro: () => set(startOutroState),
advanceGameState: () => advanceGameState: () =>
set((state) => { set((state) => {
@@ -219,31 +282,8 @@ export const useGameStore = create<GameStore>()((set) => ({
return completeIntroState(state); return completeIntroState(state);
} }
if (state.mainState === "bike") { if (isRepairMissionId(state.mainState)) {
const nextStep = getNextMissionStep(state.bike.currentStep); return advanceRepairMissionState(state, state.mainState);
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 } };
} }
return startOutroState(state); return startOutroState(state);
@@ -254,31 +294,8 @@ export const useGameStore = create<GameStore>()((set) => ({
return { intro: { ...state.intro, hasCompleted: false } }; return { intro: { ...state.intro, hasCompleted: false } };
} }
if (state.mainState === "bike") { if (isRepairMissionId(state.mainState)) {
return { return rewindRepairMissionState(state, state.mainState);
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),
},
};
} }
return { outro: { ...state.outro, hasStarted: false } }; return { outro: { ...state.outro, hasStarted: false } };