Feat/repair game #2
+1
-1
@@ -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`
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 } };
|
||||||
|
|||||||
Reference in New Issue
Block a user