Update README.md
This commit is contained in:
@@ -25,11 +25,6 @@ Built with React, Three.js, and Vite. Runs in the browser, no installation requi
|
|||||||
| [@react-three/postprocessing](https://github.com/pmndrs/postprocessing) | https://github.com/pmndrs/postprocessing |
|
| [@react-three/postprocessing](https://github.com/pmndrs/postprocessing) | https://github.com/pmndrs/postprocessing |
|
||||||
| [GSAP](https://gsap.com/docs/v3/Installation/) | https://gsap.com/docs/v3/ |
|
| [GSAP](https://gsap.com/docs/v3/Installation/) | https://gsap.com/docs/v3/ |
|
||||||
|
|
||||||
### State
|
|
||||||
| Package | Doc |
|
|
||||||
|--------|-----|
|
|
||||||
| [Zustand](https://zustand.docs.pmnd.rs/) | https://zustand.docs.pmnd.rs/ |
|
|
||||||
|
|
||||||
### Performance & Effects
|
### Performance & Effects
|
||||||
| Package | Doc |
|
| Package | Doc |
|
||||||
|--------|-----|
|
|--------|-----|
|
||||||
@@ -76,13 +71,13 @@ la-fabrik/
|
|||||||
│ └── LoadingScreen.tsx # Asset progress
|
│ └── LoadingScreen.tsx # Asset progress
|
||||||
│
|
│
|
||||||
├── stateManager/ # All logic, state, orchestration
|
├── stateManager/ # All logic, state, orchestration
|
||||||
│ ├── GameManager.ts # Orchestrator: phase, missions, steps
|
│ ├── GameManager.ts # Single source of truth: phase, zone, mission
|
||||||
│ ├── CinematicManager.ts # GSAP timelines, camera lock/unlock
|
│ ├── CinematicManager.ts # GSAP timelines, camera lock/unlock
|
||||||
│ ├── AudioManager.ts # Music, SFX, spatial audio
|
│ ├── AudioManager.ts # Music, SFX, spatial audio
|
||||||
│ ├── NPCManager.ts # Dialogues, NPC state
|
|
||||||
│ └── ZoneManager.ts # Zone detection, LOD triggers
|
│ └── ZoneManager.ts # Zone detection, LOD triggers
|
||||||
│
|
│
|
||||||
├── hooks/ # React hooks — thin wrappers on managers
|
├── hooks/ # React hooks — thin wrappers on managers
|
||||||
|
│ ├── useGameState.ts # Subscribes to GameManager
|
||||||
│ ├── useZoneDetection.ts
|
│ ├── useZoneDetection.ts
|
||||||
│ ├── useInteraction.ts
|
│ ├── useInteraction.ts
|
||||||
│ ├── useCinematic.ts
|
│ ├── useCinematic.ts
|
||||||
@@ -91,7 +86,8 @@ la-fabrik/
|
|||||||
│
|
│
|
||||||
├── data/
|
├── data/
|
||||||
│ ├── zones.ts # { id, position, radius, missionId }
|
│ ├── zones.ts # { id, position, radius, missionId }
|
||||||
│ └── dialogues.ts # Narrative scripts per zone
|
│ ├── dialogues.ts # Narrative scripts, PNJ states
|
||||||
|
│ └── missions.ts # Mission definitions, steps
|
||||||
│
|
│
|
||||||
├── shaders/
|
├── shaders/
|
||||||
│ └── hologram/
|
│ └── hologram/
|
||||||
@@ -100,8 +96,6 @@ la-fabrik/
|
|||||||
│
|
│
|
||||||
├── utils/
|
├── utils/
|
||||||
│ ├── Debug.ts # lil-gui panel
|
│ ├── Debug.ts # lil-gui panel
|
||||||
│ ├── Sizes.ts # Viewport dimensions, resize listener
|
|
||||||
│ ├── Time.ts # Delta, elapsed — outside useFrame
|
|
||||||
│ ├── EventEmitter.ts # Decoupled event bus between managers
|
│ ├── EventEmitter.ts # Decoupled event bus between managers
|
||||||
│ └── Dispose.ts # traverse() + dispose() helper
|
│ └── Dispose.ts # traverse() + dispose() helper
|
||||||
│
|
│
|
||||||
@@ -199,11 +193,101 @@ export class WorkshopZone implements WorldObject {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. Memory Management — `traverse()` + `dispose()`
|
### 3. State Management — Single Source of Truth
|
||||||
|
|
||||||
**Every `destroy()` must call `Dispose.object()` before removing anything from the scene.** Skipping this leaks GPU memory (VRAM) silently — no error thrown, just a crash after a few zone transitions.
|
The project uses a single authoritative `GameManager` for durable gameplay state. React components subscribe to this state through thin custom hooks. **High-frequency values such as movement, camera interpolation, or physics never go through React state and stay in refs or frame-based systems.**
|
||||||
|
|
||||||
**Rule: traverse first, remove second. Always.**
|
```ts
|
||||||
|
// GameManager.ts — single source of truth
|
||||||
|
type Phase = 'loading' | 'intro' | 'exploring' | 'cinematic' | 'outro'
|
||||||
|
type ZoneId = 'workshop' | 'powerGrid' | 'farm' | null
|
||||||
|
|
||||||
|
type GameSnapshot = {
|
||||||
|
phase: Phase
|
||||||
|
activeZone: ZoneId
|
||||||
|
missionId: string | null
|
||||||
|
missionStep: number
|
||||||
|
inputLocked: boolean
|
||||||
|
dialogueId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GameManager {
|
||||||
|
private static _instance: GameManager | null = null
|
||||||
|
private listeners = new Set<() => void>()
|
||||||
|
|
||||||
|
private state: GameSnapshot = {
|
||||||
|
phase: 'loading',
|
||||||
|
activeZone: null,
|
||||||
|
missionId: null,
|
||||||
|
missionStep: 0,
|
||||||
|
inputLocked: false,
|
||||||
|
dialogueId: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): GameManager {
|
||||||
|
if (!GameManager._instance) {
|
||||||
|
GameManager._instance = new GameManager()
|
||||||
|
}
|
||||||
|
return GameManager._instance
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(): GameSnapshot {
|
||||||
|
return this.state
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(listener: () => void): () => void {
|
||||||
|
this.listeners.add(listener)
|
||||||
|
return () => this.listeners.delete(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit(): void {
|
||||||
|
this.listeners.forEach((cb) => cb())
|
||||||
|
}
|
||||||
|
|
||||||
|
setPhase(phase: Phase): void {
|
||||||
|
this.state.phase = phase
|
||||||
|
this.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveZone(zone: ZoneId): void {
|
||||||
|
this.state.activeZone = zone
|
||||||
|
this.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
startMission(id: string): void {
|
||||||
|
this.state.missionId = id
|
||||||
|
this.state.missionStep = 0
|
||||||
|
this.emit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// hooks/useGameState.ts
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { GameManager } from '@/stateManager/GameManager'
|
||||||
|
|
||||||
|
export function useGameState() {
|
||||||
|
const game = GameManager.getInstance()
|
||||||
|
const [state, setState] = useState(game.getState())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return game.subscribe(() => {
|
||||||
|
setState({ ...game.getState() })
|
||||||
|
})
|
||||||
|
}, [game])
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All other managers (Cinemactic, Audio, Zone) remain as side effects that communicate through `GameManager`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Memory Management — `traverse()` + `dispose()`
|
||||||
|
|
||||||
|
**Every `destroy()` must call `Dispose.object()` before removing anything from the scene.** Skipping this leaks GPU memory (VRAM) silently — no error thrown, just a crash after a few zone transitions. **Rule: traverse first, remove second. Always.**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// utils/Dispose.ts
|
// utils/Dispose.ts
|
||||||
@@ -262,61 +346,6 @@ destroy(): void {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. Manager Coordination
|
|
||||||
|
|
||||||
`GameManager` is the **single entry point** for all logic. Components and hooks never import `CinematicManager` or `AudioManager` directly — always through `GameManager`. This keeps the dependency graph flat and every interaction auditable from one place.
|
|
||||||
|
|
||||||
```
|
|
||||||
Component / Hook
|
|
||||||
↓
|
|
||||||
GameManager.getInstance()
|
|
||||||
├── .cinematic.play('intro_workshop')
|
|
||||||
├── .audio.playAmbience('workshop')
|
|
||||||
├── .zone.setActive('workshop')
|
|
||||||
└── .npc.startDialogue('mechanic_greeting')
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// hooks/useCinematic.ts — thin wrapper, no logic
|
|
||||||
export function useCinematic() {
|
|
||||||
const trigger = useCallback((id: string) => {
|
|
||||||
GameManager.getInstance().cinematic.play(id)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return { trigger }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Zustand is used **only for UI reactivity** — to push state from managers into React components. The logic lives in the manager class, not in the store.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// stateManager/GameManager.ts — Zustand as a thin reactive bridge
|
|
||||||
import { create } from 'zustand'
|
|
||||||
|
|
||||||
type GameState = {
|
|
||||||
phase: 'loading' | 'intro' | 'exploring' | 'cinematic' | 'outro'
|
|
||||||
activeZone: 'workshop' | 'powerGrid' | 'farm' | null
|
|
||||||
setPhase: (phase: GameState['phase']) => void
|
|
||||||
setActiveZone: (zone: GameState['activeZone']) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGameStore = create<GameState>((set) => ({
|
|
||||||
phase: 'loading',
|
|
||||||
activeZone: null,
|
|
||||||
setPhase: (phase) => set({ phase }),
|
|
||||||
setActiveZone: (zone) => set({ activeZone: zone }),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// GameManager writes — React components read
|
|
||||||
export class GameManager {
|
|
||||||
setPhase(phase: GameState['phase']): void {
|
|
||||||
useGameStore.getState().setPhase(phase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. Debug Utility
|
### 5. Debug Utility
|
||||||
|
|
||||||
Activate the debug panel by appending `?debug` to the URL (`http://localhost:5173?debug`). Never scatter `if (isDev)` blocks across files — all debug logic flows through `Debug.ts`.
|
Activate the debug panel by appending `?debug` to the URL (`http://localhost:5173?debug`). Never scatter `if (isDev)` blocks across files — all debug logic flows through `Debug.ts`.
|
||||||
|
|||||||
Reference in New Issue
Block a user