240 lines
6.8 KiB
Markdown
240 lines
6.8 KiB
Markdown
# Zustand Stores
|
|
|
|
This document explains how Zustand is used in the current project.
|
|
|
|
## Why Zustand Exists Here
|
|
|
|
The project needs shared state that is durable enough to be read by multiple React and React Three Fiber systems.
|
|
|
|
Zustand is used for:
|
|
|
|
- game progression
|
|
- settings
|
|
- subtitle display
|
|
|
|
It is not used for high-frequency frame values. Values such as player velocity, temporary vectors, object positions during a grab, raycasts, and animation-loop data stay in refs or manager-local state.
|
|
|
|
## Store Locations
|
|
|
|
Current Zustand stores:
|
|
|
|
```txt
|
|
src/managers/stores/useGameStore.ts
|
|
src/managers/stores/useSettingsStore.ts
|
|
src/managers/stores/useSubtitleStore.ts
|
|
```
|
|
|
|
They are under `src/managers/stores/` because they are shared runtime state, not state owned by one visual component.
|
|
|
|
## Store Responsibilities
|
|
|
|
| Store | Responsibility |
|
|
| ------------------ | ------------------------------------------------------------- |
|
|
| `useGameStore` | Durable game progression, mission steps, cinematic input lock |
|
|
| `useSettingsStore` | Menu visibility, volumes, and subtitle options |
|
|
| `useSubtitleStore` | Currently displayed subtitle cue |
|
|
|
|
## Managers vs Stores
|
|
|
|
Managers own imperative runtime objects and side effects.
|
|
|
|
Examples:
|
|
|
|
- `AudioManager` owns audio elements, music playback, sound pools, category volumes, and optional panner nodes.
|
|
- `InteractionManager` owns transient interaction handles and input-oriented focus/holding state.
|
|
|
|
Stores own durable shared state:
|
|
|
|
- current game phase
|
|
- mission sub-step
|
|
- progression flags
|
|
- settings values
|
|
- currently displayed subtitle cue
|
|
|
|
Rule of thumb:
|
|
|
|
- manager = runtime objects, side effects, frame-adjacent imperative logic
|
|
- store = shared state that UI, world, or gameplay components need to subscribe to
|
|
|
|
## Game Store Shape
|
|
|
|
`useGameStore` exposes the main game progression.
|
|
|
|
Main states:
|
|
|
|
| Main state | Role |
|
|
| ---------- | ------------------------------- |
|
|
| `intro` | Onboarding and opening sequence |
|
|
| `ebike` | E-bike repair sequence |
|
|
| `pylon` | Power pylon repair sequence |
|
|
| `farm` | Vertical farm repair sequence |
|
|
| `outro` | Ending sequence |
|
|
|
|
Other important state:
|
|
|
|
- `isCinematicPlaying`
|
|
- `intro`
|
|
- `ebike`
|
|
- `pylon`
|
|
- `farm`
|
|
- `outro`
|
|
|
|
Mission steps:
|
|
|
|
```ts
|
|
"locked" |
|
|
"waiting" |
|
|
"inspected" |
|
|
"fragmented" |
|
|
"scanning" |
|
|
"repairing" |
|
|
"reassembling" |
|
|
"done";
|
|
```
|
|
|
|
`isCinematicPlaying` is read by `PlayerController` to ignore player input while camera timelines are active.
|
|
|
|
## 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 Game 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("ebike");
|
|
```
|
|
|
|
Direct setters are useful for debug panels, but production gameplay should prefer business actions such as:
|
|
|
|
- `advanceGameState`
|
|
- `completeEbike`
|
|
- `completePylon`
|
|
- `completeFarm`
|
|
- `completeMission`
|
|
|
|
Mission gameplay that can target `ebike`, `pylon`, or `farm` should prefer generic mission actions:
|
|
|
|
```ts
|
|
const setMissionStep = useGameStore((state) => state.setMissionStep);
|
|
const completeMission = useGameStore((state) => state.completeMission);
|
|
|
|
setMissionStep("ebike", "inspected");
|
|
completeMission("ebike");
|
|
```
|
|
|
|
This keeps reusable gameplay components such as `RepairGame` from duplicating mission-specific branches like `setEbikeState`, `setPylonState`, and `setFarmState`.
|
|
|
|
## Settings Store
|
|
|
|
`useSettingsStore` owns player-facing settings and forwards audio volume changes to `AudioManager`.
|
|
|
|
State:
|
|
|
|
- `isSettingsMenuOpen`
|
|
- `musicVolume`
|
|
- `sfxVolume`
|
|
- `dialogueVolume`
|
|
- `subtitlesEnabled`
|
|
- `subtitleLanguage`
|
|
|
|
Audio setters clamp values between `0` and `1`, then call:
|
|
|
|
```ts
|
|
AudioManager.getInstance().setCategoryVolume(category, nextVolume);
|
|
```
|
|
|
|
This keeps UI state and browser audio state synchronized.
|
|
|
|
## Subtitle Store
|
|
|
|
`useSubtitleStore` is intentionally tiny.
|
|
|
|
State/actions:
|
|
|
|
- `activeSubtitle`
|
|
- `setActiveSubtitle`
|
|
- `clearActiveSubtitle`
|
|
|
|
`playDialogueById()` writes to this store while dialogue audio plays. `Subtitles` reads from it and respects `useSettingsStore().subtitlesEnabled`.
|
|
|
|
## World Integration
|
|
|
|
`src/world/GameStageContent.tsx` subscribes to `mainState` and mounts the repair-game content.
|
|
|
|
Current production repair placement:
|
|
|
|
```tsx
|
|
<RepairGame mission="ebike" position={[42.2399, 4.5484, 34.6468]} />
|
|
<RepairGame mission="pylon" position={[64, 0, -66]} />
|
|
<RepairGame mission="farm" position={[-24, 0, 42]} />
|
|
```
|
|
|
|
`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:
|
|
|
|
```txt
|
|
src/types/gameplay/repairMission.ts
|
|
```
|
|
|
|
Mission-specific behavior stays in:
|
|
|
|
```txt
|
|
src/data/gameplay/repairMissions.ts
|
|
```
|
|
|
|
That lets the repair flow stay reusable while each mission defines its own model, broken parts, replacement parts, prompts, and timing.
|
|
|
|
## 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`
|
|
- `GameStateDebugPanel`: compact debug UI for viewing and switching main/sub states
|
|
- `Crosshair`: player aiming helper
|
|
- `InteractPrompt`: interaction prompt
|
|
- `RepairMovementLockIndicator`: indicator shown while repair steps lock movement
|
|
- `HandTrackingVisualizer`: hand tracking SVG fallback/debug visualization
|
|
- `Subtitles`: active dialogue subtitle overlay
|
|
- `GameSettingsMenu`: options menu and settings controls
|
|
|
|
## 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.
|
|
- Keep settings side effects, such as audio category updates, inside settings actions rather than spreading them across UI components.
|
|
|
|
## Next Steps
|
|
|
|
- Move broader mission orchestration into a clearer layer if intro, mission, dialogue, and cinematic branching grows.
|