Files
La-Fabrik/docs/technical/zustand.md
T
Tom Boullay 836d591617
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
refactor: move mission flow state into game store
2026-05-11 18:02:00 +02:00

195 lines
7.0 KiB
Markdown

# Zustand Game State
This document explains how Zustand is used in the current project.
## Why Zustand Exists Here
The project needs one shared source of truth for the player's progression through the experience.
The current progression is split into main states:
| Main state | Role |
| ---------- | ------------------------------- |
| `intro` | Onboarding and opening sequence |
| `bike` | E-bike repair sequence |
| `pylone` | Power grid sequence |
| `ferme` | Vertical farm sequence |
| `outro` | Ending sequence |
Each main state can also own smaller sub state, such as the current mission step, dialogue audio, or completion flags.
Zustand is useful because React and React Three Fiber components can subscribe only to the state slice they need. When that slice changes, only the subscribed components re-render.
## Store Location
The game progression store lives here:
```txt
src/managers/stores/useGameStore.ts
```
The store is placed under `src/managers/stores/` because it belongs to the gameplay orchestration layer, not to a specific visual component.
## Managers vs Store
Managers are responsible for local runtime objects and imperative behavior.
Examples:
- `AudioManager` owns audio elements and sound pools.
- `InteractionManager` owns transient interaction handles and input-oriented behavior.
Managers can read from or write to the Zustand store when their local behavior needs to affect global gameplay progression.
The Zustand store is responsible for durable global state:
- current main state
- mission sub state
- progression flags
- dialogue/audio references
- state transitions
Rule of thumb:
- manager = runtime objects, side effects, and local imperative logic
- store = global gameplay state that UI or world components can subscribe to
## Current Shape
The store exposes:
- `mainState`: the active game phase
- `missionFlow`: intro and mission 2 prototype state
- `intro`: intro-specific state
- `bike`: e-bike mission state
- `pylone`: power grid mission state
- `ferme`: farm mission state
- `outro`: ending state
- actions for direct updates and progression updates
The `missionFlow` slice contains the prototype step, player name, movement lock, city activity flag, and temporary dialog message. It is in the main game store because it is global gameplay state used by UI, world components, and the player controller.
The mission steps currently use this sequence:
```ts
"locked" |
"waiting" |
"inspected" |
"fragmented" |
"scanning" |
"repairing" |
"reassembling" |
"done";
```
## Reading State In Components
Use selectors to read only what the component needs.
```tsx
import { useGameStore } from "@/managers/stores/useGameStore";
export function Example(): React.JSX.Element {
const mainState = useGameStore((state) => state.mainState);
return <p>Current state: {mainState}</p>;
}
```
This is better than reading the whole store, because the component re-renders only when `mainState` changes.
## Updating State
Prefer explicit actions from the store.
```ts
const advanceGameState = useGameStore((state) => state.advanceGameState);
advanceGameState();
```
For development and debug tooling, direct setters also exist:
```ts
const setMainState = useGameStore((state) => state.setMainState);
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.
For repair missions, it mounts the reusable `RepairGame` component with a mission id:
```tsx
<RepairGame mission="bike" position={[8, 0, -6]} />
```
`RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep` and `completeMission`. Shared repair ids, mission steps, and runtime guards live in `src/types/gameplay/repairMission.ts` so static mission config does not depend on the Zustand store. The production repair flow currently supports `waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission` state transitions.
Mission-specific behavior stays in `src/data/gameplay/repairMissions.ts`: each mission can define its broken nodes, placeholder targets, scan duration, and reassembly duration without adding mission branches to `RepairGame`.
The intro and mission 2 prototype flow is documented separately in `docs/technical/mission-flow.md`. It intentionally uses the same `useGameStore` source of truth instead of a dedicated `GameStepManager` or a second Zustand store.
That means the scene can progressively move toward this pattern:
```tsx
switch (mainState) {
case "intro":
return <IntroContent />;
case "bike":
return <BikeContent />;
case "pylone":
return <PyloneContent />;
case "ferme":
return <FarmContent />;
case "outro":
return <OutroContent />;
}
```
In React Three Fiber, mounting and unmounting JSX controls what appears in the Three.js scene. When a state-specific component disappears from JSX, React removes it from the scene.
## UI Integration
`src/components/ui/GameUI.tsx` groups the HTML overlays used by the playable route.
Current overlays:
- `DebugOverlayLayout`: debug-only overlay shown with `?debug`, including the `GameStateDebugPanel` progression panel
- `GameStateDebugPanel`: compact debug UI for viewing and switching main/sub states, stepping backward or forward, and resetting the store
- `Crosshair`: player aiming helper
- `InteractPrompt`: interaction prompt
- `RepairMovementLockIndicator`: player-facing indicator shown while repair steps temporarily disable movement
- Mission flow overlays such as `IntroUI`, `BienvenueDisplay`, and `DialogMessage` are mounted by `src/pages/page.tsx` because they are route-level HTML overlays rather than persistent game HUD elements.
`src/pages/page.tsx` should stay thin and mount the canvas, persistent `GameUI`, and route-level overlays.
## Regression Rules
- Do not store per-frame values in Zustand.
- Use `useRef` for high-frequency mutable values such as player velocity, temporary vectors, or animation-loop data.
- Use selectors instead of reading the whole store in components.
- Keep gameplay transitions inside store actions when possible.
- Keep debug-only controls behind `?debug`.
- Add new state only when a real runtime feature needs it.
## Next Steps
Move repair validation into mission data once each mission has distinct broken module nodes, replacement assets, and completion events.