add : finish readme, git lfs and gitignore

This commit is contained in:
2026-04-13 23:30:33 +02:00
parent 9966bb8e25
commit c12026a331
3 changed files with 258 additions and 126 deletions
+24
View File
@@ -0,0 +1,24 @@
# Git LFS for 3D assets
# Track large binary files to avoid repo bloat
*.glb filter=lfs diff=lfs merge=lfs -text
*.gltf filter=lfs diff=lfs merge=lfs -text
# Textures
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text
*.hdr filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
*.ktx2 filter=lfs diff=lfs merge=lfs -text
*.ktx filter=lfs diff=lfs merge=lfs -text
# Audio
*.wav filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
# Video (cinematics)
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text
+27 -11
View File
@@ -1,24 +1,40 @@
# Dependencies
node_modules/
# Build
dist/
dist-ssr/
*.local
# Environment
.env
.env.local
.env.*.local
# Logs # Logs
logs logs/
*.log *.log
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log*
node_modules # Editor
dist
dist-ssr
*.local
# Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea/
.DS_Store
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw?
# OS
.DS_Store
Thumbs.db
# Debug
*.debug
# 3D Assets Cache (drei, GLTFJSX)
.drei/
.glitchdrei-cache/
+171 -79
View File
@@ -48,6 +48,7 @@ la-fabrik/
└── src/ └── src/
├── world/ # Single persistent 3D world ├── world/ # Single persistent 3D world
│ ├── Map.tsx # Base map, always mounted │ ├── Map.tsx # Base map, always mounted
│ ├── Lighting.tsx # Ambient, directional, point lights
│ ├── Environment.tsx # HDRI, fog, sky │ ├── Environment.tsx # HDRI, fog, sky
│ ├── PostFX.tsx # Bloom, SSAO, chromatic aberration │ ├── PostFX.tsx # Bloom, SSAO, chromatic aberration
│ ├── zones/ # Spatial zones — LOD per zone │ ├── zones/ # Spatial zones — LOD per zone
@@ -96,7 +97,7 @@ la-fabrik/
├── utils/ ├── utils/
│ ├── Debug.ts # lil-gui panel │ ├── Debug.ts # lil-gui panel
│ ├── EventEmitter.ts # Decoupled event bus between managers │ ├── EventEmitter.ts # Simple pub/sub for manager-to-manager events
│ └── Dispose.ts # traverse() + dispose() helper │ └── Dispose.ts # traverse() + dispose() helper
├── App.tsx # Canvas + UI superimposed ├── App.tsx # Canvas + UI superimposed
@@ -105,11 +106,31 @@ la-fabrik/
## 🏗 Architecture Patterns ## 🏗 Architecture Patterns
These patterns are **mandatory across the entire codebase**. Every class, every 3D object, every manager follows the same conventions. Consistency over cleverness. The project uses **two complementary patterns**:
### 1. Singleton Pattern - **Singleton service classes** for orchestration and side effects
- **Declarative React components** for all 3D scene objects
Every Manager and core utility uses the same singleton pattern. One instance, shared everywhere — no prop drilling, no context, no stray `new` calls This distinction is intentional. Scene elements such as the map, lights, environment, zones, and player are implemented as **React Three Fiber components** and mounted through `<Canvas>`.
Global systems such as gameplay flow, cinematics, audio, and debug tooling are implemented as **manager classes**.
Consistency matters, but the codebase does **not** force the same lifecycle pattern on scene components and global services.
---
### 1. Singleton Pattern for Global Managers Only
Only cross-cutting services use the singleton pattern.
Examples:
- `GameManager`
- `CinematicManager`
- `AudioManager`
- `ZoneManager`
- `Debug`
- `EventEmitter`
These services must exist once, be accessible from anywhere, and coordinate the experience globally.
```ts ```ts
// stateManager/GameManager.ts // stateManager/GameManager.ts
@@ -119,7 +140,6 @@ export class GameManager {
cinematic!: CinematicManager cinematic!: CinematicManager
audio!: AudioManager audio!: AudioManager
zone!: ZoneManager zone!: ZoneManager
npc!: NPCManager
static getInstance(): GameManager { static getInstance(): GameManager {
if (!GameManager._instance) { if (!GameManager._instance) {
@@ -132,73 +152,95 @@ export class GameManager {
this.cinematic = CinematicManager.getInstance() this.cinematic = CinematicManager.getInstance()
this.audio = AudioManager.getInstance() this.audio = AudioManager.getInstance()
this.zone = ZoneManager.getInstance() this.zone = ZoneManager.getInstance()
this.npc = NPCManager.getInstance()
} }
destroy(): void { destroy(): void {
this.cinematic.destroy() this.cinematic.destroy()
this.audio.destroy() this.audio.destroy()
this.zone.destroy() this.zone.destroy()
this.npc.destroy()
GameManager._instance = null GameManager._instance = null
} }
} }
```
// Usage — anywhere in the codebase Usage:
```ts
const game = GameManager.getInstance() const game = GameManager.getInstance()
game.startMission('workshop') game.startMission('workshop')
``` ```
Apply the **exact same pattern** to every Manager and utility class (`CinematicManager`, `AudioManager`, `Debug`, `Sizes`, `EventEmitter`) **Important:** scene objects such as `Map`, `WorkshopZone`, `Lighting`, or `Environment` are **not** singletons and must remain standard React components.
--- ---
### 2. Class Interface — `load` / `update` / `destroy` ### 2. Scene Objects Are React Components, Not Manager Classes
Every 3D class implements the same three-method lifecycle. No exceptions. All 3D scene objects are implemented as **declarative React components**.
```ts This includes:
// Enforced interface for all world objects - maps
interface WorldObject { - lights
load(): Promise<void> // Load assets, build scene graph - environments
update(delta: number): void // Per-frame logic (called from useFrame) - player controllers
destroy(): void // Clean GPU memory — mandatory - zones
} - interactive props
``` - postprocessing layers
```ts This keeps the code aligned with the R3F runtime instead of rebuilding a parallel imperative engine.
Example:
```tsx
// world/zones/WorkshopZone.tsx // world/zones/WorkshopZone.tsx
export class WorkshopZone implements WorldObject { import { useEffect, useRef } from 'react'
private group: THREE.Group = new THREE.Group() import * as THREE from 'three'
private mixer: THREE.AnimationMixer | null = null import { useFrame } from '@react-three/fiber'
import { useGLTF } from '@react-three/drei'
async load(): Promise<void> { export function WorkshopZone() {
const gltf = await loadGLTF('/models/workshop/ebike.glb') const root = useRef<THREE.Group>(null)
this.group = gltf.scene const gltf = useGLTF('/models/workshop/ebike.glb')
this.mixer = new THREE.AnimationMixer(this.group) const mixer = useRef<THREE.AnimationMixer | null>(null)
this.scene.add(this.group)
}
update(delta: number): void { useEffect(() => {
this.mixer?.update(delta) mixer.current = new THREE.AnimationMixer(gltf.scene)
}
destroy(): void { return () => {
this.mixer?.stopAllAction() mixer.current?.stopAllAction()
Dispose.object(this.group) // ← always traverse before remove mixer.current = null
this.scene.remove(this.group)
} }
}, [gltf.scene])
useFrame((_, delta) => {
mixer.current?.update(delta)
})
return <primitive ref={root} object={gltf.scene.clone()} />
} }
``` ```
Per-frame values such as movement, interpolation, camera smoothing, and physics must stay in:
- `useRef`
- `useFrame`
- Rapier bodies
- other frame-based systems
They must **never** go through React state.
--- ---
### 3. State Management — Single Source of Truth ### 3. Single Source of Truth for Durable Gameplay State
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.** The project uses a single authoritative `GameManager` for durable gameplay state.
React components subscribe to that state through thin hooks.
Other managers communicate through `GameManager`, which acts as the main gameplay orchestrator.
High-frequency values such as movement, camera interpolation, or physics never go through React state and stay in refs or frame-based systems.
```ts ```ts
// GameManager.ts — single source of truth // stateManager/GameManager.ts
type Phase = 'loading' | 'intro' | 'exploring' | 'cinematic' | 'outro' type Phase = 'loading' | 'intro' | 'exploring' | 'cinematic' | 'outro'
type ZoneId = 'workshop' | 'powerGrid' | 'farm' | null type ZoneId = 'workshop' | 'powerGrid' | 'farm' | null
@@ -281,53 +323,87 @@ export function useGameState() {
} }
``` ```
All other managers (Cinemactic, Audio, Zone) remain as side effects that communicate through `GameManager` This keeps the architecture simple:
- **GameManager** owns durable gameplay state
- **other managers** handle side effects
- **React components** render that state
- **R3F frame systems** handle fast-changing values
--- ---
### 4. Memory Management — `traverse()` + `dispose()` ### 4. Side Effects Stay in Specialized Managers
**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.** Managers other than `GameManager` should not become secondary state stores.
Their role is to manage side effects and specialized runtime logic, such as:
- GSAP timelines
- audio playback
- zone entry detection
- interaction triggers
- camera lock/unlock
- temporary event coordination
They can read from `GameManager`, react to its state, or notify it of important transitions.
Example flow:
```text
Component / Hook
GameManager.getInstance()
├── startMission('workshop')
├── cinematic.play('intro_workshop')
├── audio.playAmbience('workshop')
└── zone.setActive('workshop')
```
This keeps the dependency graph understandable while avoiding duplicated durable state.
---
### 5. Memory Management — Dispose Only What You Own
GPU memory must be cleaned carefully.
However, the project does **not** blindly deep-dispose every object on unmount.
Only resources explicitly created and owned by the current component or manager should be disposed.
This includes things like:
- custom materials
- render targets
- postprocessing passes
- manually created geometries
- manually created textures
- temporary clones with owned resources
Shared or cached assets must **not** be blindly disposed.
```ts ```ts
// utils/Dispose.ts // utils/Dispose.ts
import * as THREE from 'three' import * as THREE from 'three'
export class Dispose { export class Dispose {
/** static material(material: THREE.Material): void {
* Recursively disposes all geometries, materials, and textures for (const value of Object.values(material)) {
* from an Object3D and its entire subtree.
*
* Always call this before scene.remove() to prevent VRAM leaks.
*/
static object(obj: THREE.Object3D): void {
obj.traverse((child) => {
if (!(child instanceof THREE.Mesh)) return
// 1. Dispose geometry buffers
child.geometry.dispose()
// 2. Handle single and multi-material meshes
const materials = Array.isArray(child.material)
? child.material
: [child.material]
for (const mat of materials) {
// 3. Dispose every texture referenced by the material
for (const value of Object.values(mat)) {
if (value instanceof THREE.Texture) { if (value instanceof THREE.Texture) {
value.dispose() value.dispose()
} }
} }
// 4. Dispose the material itself material.dispose()
mat.dispose() }
}
}) static mesh(mesh: THREE.Mesh): void {
mesh.geometry?.dispose()
const materials = Array.isArray(mesh.material)
? mesh.material
: [mesh.material]
for (const material of materials) {
if (material) this.material(material)
}
} }
/**
* Dispose a WebGL render target and its textures.
*/
static renderTarget(rt: THREE.WebGLRenderTarget): void { static renderTarget(rt: THREE.WebGLRenderTarget): void {
rt.texture.dispose() rt.texture.dispose()
rt.dispose() rt.dispose()
@@ -335,20 +411,35 @@ export class Dispose {
} }
``` ```
Usage pattern — identical in every `destroy()`: Example usage:
```ts ```ts
destroy(): void { useEffect(() => {
Dispose.object(this.mesh) // Frees VRAM: geometries, materials, textures const material = new THREE.ShaderMaterial({
this.scene.remove(this.mesh) // Then removes from scene graph vertexShader,
fragmentShader,
})
meshRef.current.material = material
return () => {
Dispose.material(material)
} }
}, [])
``` ```
**Rule:** disposal is ownership-based, not automatic and not blind.
--- ---
### 5. Debug Utility ### 6. 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`. The debug panel can be activated by appending `?debug` to the URL:
`http://localhost:5173?debug`
All debug logic is centralized in `Debug.ts`.
Do not scatter debug checks across the codebase.
```ts ```ts
// utils/Debug.ts // utils/Debug.ts
@@ -379,15 +470,16 @@ export class Debug {
} }
``` ```
Usage:
```ts ```ts
// Usage in any class
const debug = Debug.getInstance() const debug = Debug.getInstance()
if (debug.active) { if (debug.active) {
debug.gui!.add(this.mesh.position, 'y', -5, 5).name('Height') debug.gui!.add(params, 'bloomIntensity', 0, 3).name('Bloom')
} }
``` ```
## 🚀 Getting Started ## 🚀 Getting Started
```bash ```bash