Merge branch 'main' into design

This commit is contained in:
2026-04-15 13:32:38 +02:00
8 changed files with 941 additions and 383 deletions
+1
View File
@@ -80,6 +80,7 @@ import { useGameState } from "@/hooks/useGameState";
See `.agent/skills/` for detailed patterns per technology: See `.agent/skills/` for detailed patterns per technology:
- `best-practices.md` — Code generation conventions (W3C, simple, scalable, modern)
- `r3f.md` — React Three Fiber component patterns - `r3f.md` — React Three Fiber component patterns
- `three.md` — Three.js conventions and AnimationMixer - `three.md` — Three.js conventions and AnimationMixer
- `gsap.md` — GSAP timeline and cinematic patterns - `gsap.md` — GSAP timeline and cinematic patterns
+320
View File
@@ -0,0 +1,320 @@
# Skill — Best Practices
## Principles
Generate code that is **simple**, **understandable**, **reviewable**, **scalable**, **optimized**, and **modern**. Follow W3C web standards and platform conventions.
## Naming Conventions
### Files
| Type | Convention | Example |
| ---------- | --------------------------- | -------------------- |
| Components | PascalCase | `WorkshopZone.tsx` |
| Hooks | camelCase with `use` prefix | `useGameState.ts` |
| Managers | PascalCase | `GameManager.ts` |
| Utils | PascalCase | `Dispose.ts` |
| Data | PascalCase | `missions.ts` |
| Shaders | kebab-case | `hologram.vert.glsl` |
### Variables & Functions
| Type | Convention | Example |
| ---------------- | -------------------- | --------------------------------------------- |
| Variables | camelCase | `activeZone`, `missionStep` |
| Functions | camelCase | `startMission()`, `setActiveZone()` |
| Constants | UPPER_SNAKE_CASE | `MAX_SPEED`, `DEFAULT_PHASE` |
| React components | PascalCase | `function WorkshopZone()` |
| React hooks | camelCase with `use` | `useGameState()`, `useFrame()` |
| Classes | PascalCase | `class GameManager` |
| Interfaces/Types | PascalCase | `type GameSnapshot`, `interface ZoneData` |
| Enums | PascalCase | `enum Phase` (prefer string literals instead) |
### CSS Classes (if applicable)
Use kebab-case:
```css
.mission-hud {
}
.narrative-overlay {
}
```
## File Structure
Every file starts with a route comment:
```ts
# route path src/world/zones/WorkshopZone.tsx
```
## Code Style
### Simplicity First
```ts
// Good — clear, direct
function getZoneRadius(zone: Zone): number {
return zone.radius;
}
// Bad — over-abstracted
function getZoneRadius(zone: Zone): number {
return zone[ZoneFields.RADIUS] ?? RADIUS_DEFAULTS[zone.type]?.default ?? 50;
}
```
### Early Return
```ts
// Good
if (!gltf) return null;
if (!visible) return null;
return <primitive object={gltf.scene} />;
// Bad
if (gltf && visible) {
return <primitive object={gltf.scene} />;
}
return null;
```
### Destructuring
```ts
// Good
const { position, rotation, scale } = object;
// Good — explicit
const game = GameManager.getInstance();
const { phase, activeZone } = game.getState();
```
### Avoid Nested Callbacks
```ts
// Good — flat structure
useEffect(() => {
const mixer = new THREE.AnimationMixer(model);
return () => mixer.stopAllAction();
}, [model]);
// Bad — nested
useEffect(() => {
useEffect(() => {
const mixer = new THREE.AnimationMixer(model);
}, [model]);
}, []);
```
## TypeScript Rules
### Explicit Types for Exports
```ts
// Good — explicit return type
export function useGameState(): GameSnapshot {
// ...
}
// Good — explicit parameter types
export function setPhase(phase: Phase): void {
// ...
}
```
### Interface over Type (for object shapes)
```ts
// Good
interface ZoneData {
id: string;
position: [number, number, number];
radius: number;
}
// OK — simple unions
type Phase = "loading" | "exploring" | "cinematic";
```
### Never use `any`
```ts
// Good
const ref = useRef<THREE.Group>(null);
// Bad
const ref = useRef<any>(null);
```
## React Patterns
### Component Structure
```tsx
# route path src/world/zones/WorkshopZone.tsx
import { useRef, useEffect } from "react";
import * as THREE from "three";
import { useFrame } from "@react-three/fiber";
import { useGLTF } from "@react-three/drei";
export function WorkshopZone() {
const ref = useRef<THREE.Group>(null);
const gltf = useGLTF("/models/workshop/ebike.glb");
useFrame((_, delta) => {
// per-frame logic
});
useEffect(() => {
// setup
return () => {
// cleanup
};
}, []);
return <primitive ref={ref} object={gltf.scene.clone()} />;
}
```
### Hooks Return Simple Values
```ts
// Good — returns simple object
export function useGameState() {
const game = GameManager.getInstance();
const [state, setState] = useState(game.getState());
useEffect(() => {
return game.subscribe(() => setState({ ...game.getState() }));
}, [game]);
return state;
}
// Bad — returns methods that modify state directly
export function useGameActions() {
const game = GameManager.getInstance();
return {
setPhase: (p: Phase) => game.setPhase(p),
startMission: (id: string) => game.startMission(id),
};
}
```
## Performance
### useRef for Mutable Values
```ts
// Good — no re-render
const position = useRef(new THREE.Vector3());
// Bad — triggers re-render every frame
const [position, setPosition] = useState(new THREE.Vector3());
```
### Memoize Expensive Computations
```ts
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(
(x: number) => doSomething(x),
[dependency],
);
```
### Lazy Loading
```ts
// Components
const HeavyComponent = lazy(() => import("./HeavyComponent"));
// Assets
useGLTF.preload("/models/map/base.glb");
```
## Scalability
### Single Responsibility
```ts
// Good — focused component
export function WorkshopZone() {
// Only handles workshop zone logic
}
// Bad — does everything
export function WorkshopZone() {
// Handles zone logic + audio + cinematics + missions
}
```
### Separate Data from View
```ts
// Good — data in src/data/
// src/data/missions.ts
export const missions = {
workshop: { steps: [...] },
};
// Bad — data inline
export function WorkshopZone() {
const missions = [{ steps: [...] }]; // scattered everywhere
}
```
### Constants for Magic Numbers
```ts
// Good
const DEFAULT_CAMERA_DISTANCE = 5;
const ZONE_DETECTION_RADIUS = 20;
// Bad
mesh.position.set(5, 0, 0); // magic number
```
## Accessibility (W3C)
### Semantic HTML
```tsx
// Good
<button onClick={handleInteract} aria-label="Interact with bike">
<Model />
</button>
// Bad
<div onClick={handleInteract}>
<Model />
</div>
```
### Keyboard Navigation
```tsx
<button
onClick={openDialogue}
onKeyDown={(e) => e.key === "Enter" && openDialogue()}
>
Talk
</button>
```
## Rules
1. **Simplicity** — Every line of code must be justified. If it's not obviously necessary, remove it.
2. **Readability** — Code is read 10x more than it's written. Optimize for the reader.
3. **Reviewability** — Pull requests should be understandable in < 5 minutes.
4. **Scalability** — Architecture should support growth without refactoring existing code.
5. **Performance** — Don't optimize prematurely, but don't introduce obvious bottlenecks.
6. **Modern** — Use ES2022+ features, TypeScript strict mode, React hooks over class lifecycle.
7. **W3C** — Follow web standards: semantic HTML, ARIA, keyboard navigation.
8. **No Over-Engineering** — Avoid patterns that add complexity without benefit (factories for 1 instance, generic wrappers for single use cases, etc.)
@@ -1,4 +1,4 @@
name: 🔍 CI name: 🔍 Lint
on: on:
pull_request: pull_request:
+82
View File
@@ -0,0 +1,82 @@
name: 📊 Quality
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main]
jobs:
security:
name: 🔒 Security Audit
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v6
- name: 🧰 Setup Node
uses: actions/setup-node@v6
with:
node-version: "20"
cache: npm
- name: 📥 Install
run: npm ci
- name: 🔒 Audit
run: npm audit --audit-level=high
continue-on-error: true
dependencies:
name: 📋 Dependency Freshness
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v6
- name: 🧰 Setup Node
uses: actions/setup-node@v6
with:
node-version: "20"
cache: npm
- name: 📥 Install
run: npm ci
- name: 📋 Check outdated
run: npm outdated --depth=0
continue-on-error: true
bundle-size:
name: 📦 Bundle Size
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v6
- name: 🧰 Setup Node
uses: actions/setup-node@v6
with:
node-version: "20"
cache: npm
- name: 📥 Install
run: npm ci
- name: 📦 Build
run: npm run build
- name: 📏 Check bundle size
run: |
# Get bundle size in KB
SIZE=$(du -k dist | cut -f1)
echo "Bundle size: ${SIZE}KB"
# Threshold: 1000KB (configurable)
THRESHOLD=1000
if [ "$SIZE" -gt "$THRESHOLD" ]; then
echo "❌ Bundle size ${SIZE}KB exceeds threshold ${THRESHOLD}KB"
exit 1
fi
echo "✅ Bundle size ${SIZE}KB is under threshold"
-382
View File
@@ -106,388 +106,6 @@ la-fabrik/
└── main.tsx └── main.tsx
``` ```
## 🏗 Architecture Patterns
The project uses **two complementary patterns**:
- **Singleton service classes** for orchestration and side effects
- **Declarative React components** for all 3D scene objects
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
// stateManager/GameManager.ts
export class GameManager {
private static _instance: GameManager | null = null;
cinematic!: CinematicManager;
audio!: AudioManager;
zone!: ZoneManager;
static getInstance(): GameManager {
if (!GameManager._instance) {
GameManager._instance = new GameManager();
}
return GameManager._instance;
}
private constructor() {
this.cinematic = CinematicManager.getInstance();
this.audio = AudioManager.getInstance();
this.zone = ZoneManager.getInstance();
}
destroy(): void {
this.cinematic.destroy();
this.audio.destroy();
this.zone.destroy();
GameManager._instance = null;
}
}
```
Usage:
```ts
const game = GameManager.getInstance();
game.startMission("workshop");
```
**Important:** scene objects such as `Map`, `WorkshopZone`, `Lighting`, or `Environment` are **not** singletons and must remain standard React components.
---
### 2. Scene Objects Are React Components, Not Manager Classes
All 3D scene objects are implemented as **declarative React components**.
This includes:
- maps
- lights
- environments
- player controllers
- zones
- interactive props
- postprocessing layers
This keeps the code aligned with the R3F runtime instead of rebuilding a parallel imperative engine.
Example:
```tsx
// world/zones/WorkshopZone.tsx
import { useEffect, useRef } from "react";
import * as THREE from "three";
import { useFrame } from "@react-three/fiber";
import { useGLTF } from "@react-three/drei";
export function WorkshopZone() {
const root = useRef<THREE.Group>(null);
const gltf = useGLTF("/models/workshop/ebike.glb");
const mixer = useRef<THREE.AnimationMixer | null>(null);
useEffect(() => {
mixer.current = new THREE.AnimationMixer(gltf.scene);
return () => {
mixer.current?.stopAllAction();
mixer.current = null;
};
}, [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. Single Source of Truth for Durable Gameplay State
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
// stateManager/GameManager.ts
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;
}
```
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. Side Effects Stay in Specialized Managers
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
// utils/Dispose.ts
import * as THREE from "three";
export class Dispose {
static material(material: THREE.Material): void {
for (const value of Object.values(material)) {
if (value instanceof THREE.Texture) {
value.dispose();
}
}
material.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);
}
}
static renderTarget(rt: THREE.WebGLRenderTarget): void {
rt.texture.dispose();
rt.dispose();
}
}
```
Example usage:
```ts
useEffect(() => {
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
});
meshRef.current.material = material;
return () => {
Dispose.material(material);
};
}, []);
```
**Rule:** disposal is ownership-based, not automatic and not blind.
---
### 6. Debug Utility
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
// utils/Debug.ts
import GUI from "lil-gui";
export class Debug {
private static _instance: Debug | null = null;
readonly active: boolean;
gui: GUI | null = null;
static getInstance(): Debug {
if (!Debug._instance) Debug._instance = new Debug();
return Debug._instance;
}
private constructor() {
this.active = new URLSearchParams(window.location.search).has("debug");
if (this.active) {
this.gui = new GUI({ title: "La-Fabrik Debug" });
}
}
destroy(): void {
this.gui?.destroy();
Debug._instance = null;
}
}
```
Usage:
```ts
const debug = Debug.getInstance();
if (debug.active) {
debug.gui!.add(params, "bloomIntensity", 0, 3).name("Bloom");
}
```
## 🚀 Getting Started ## 🚀 Getting Started
```bash ```bash
+381
View File
@@ -0,0 +1,381 @@
# Architecture Patterns
The project uses **two complementary patterns**:
- **Singleton service classes** for orchestration and side effects
- **Declarative React components** for all 3D scene objects
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
// stateManager/GameManager.ts
export class GameManager {
private static _instance: GameManager | null = null;
cinematic!: CinematicManager;
audio!: AudioManager;
zone!: ZoneManager;
static getInstance(): GameManager {
if (!GameManager._instance) {
GameManager._instance = new GameManager();
}
return GameManager._instance;
}
private constructor() {
this.cinematic = CinematicManager.getInstance();
this.audio = AudioManager.getInstance();
this.zone = ZoneManager.getInstance();
}
destroy(): void {
this.cinematic.destroy();
this.audio.destroy();
this.zone.destroy();
GameManager._instance = null;
}
}
```
Usage:
```ts
const game = GameManager.getInstance();
game.startMission("workshop");
```
**Important:** scene objects such as `Map`, `WorkshopZone`, `Lighting`, or `Environment` are **not** singletons and must remain standard React components.
---
## 2. Scene Objects Are React Components, Not Manager Classes
All 3D scene objects are implemented as **declarative React components**.
This includes:
- maps
- lights
- environments
- player controllers
- zones
- interactive props
- postprocessing layers
This keeps the code aligned with the R3F runtime instead of rebuilding a parallel imperative engine.
Example:
```tsx
// world/zones/WorkshopZone.tsx
import { useEffect, useRef } from "react";
import * as THREE from "three";
import { useFrame } from "@react-three/fiber";
import { useGLTF } from "@react-three/drei";
export function WorkshopZone() {
const root = useRef<THREE.Group>(null);
const gltf = useGLTF("/models/workshop/ebike.glb");
const mixer = useRef<THREE.AnimationMixer | null>(null);
useEffect(() => {
mixer.current = new THREE.AnimationMixer(gltf.scene);
return () => {
mixer.current?.stopAllAction();
mixer.current = null;
};
}, [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. Single Source of Truth for Durable Gameplay State
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
// stateManager/GameManager.ts
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;
}
```
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. Side Effects Stay in Specialized Managers
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:
```
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
// utils/Dispose.ts
import * as THREE from "three";
export class Dispose {
static material(material: THREE.Material): void {
for (const value of Object.values(material)) {
if (value instanceof THREE.Texture) {
value.dispose();
}
}
material.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);
}
}
static renderTarget(rt: THREE.WebGLRenderTarget): void {
rt.texture.dispose();
rt.dispose();
}
}
```
Example usage:
```ts
useEffect(() => {
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
});
meshRef.current.material = material;
return () => {
Dispose.material(material);
};
}, []);
```
**Rule:** disposal is ownership-based, not automatic and not blind.
---
## 6. Debug Utility
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
// utils/Debug.ts
import GUI from "lil-gui";
export class Debug {
private static _instance: Debug | null = null;
readonly active: boolean;
gui: GUI | null = null;
static getInstance(): Debug {
if (!Debug._instance) Debug._instance = new Debug();
return Debug._instance;
}
private constructor() {
this.active = new URLSearchParams(window.location.search).has("debug");
if (this.active) {
this.gui = new GUI({ title: "La-Fabrik Debug" });
}
}
destroy(): void {
this.gui?.destroy();
Debug._instance = null;
}
}
```
Usage:
```ts
const debug = Debug.getInstance();
if (debug.active) {
debug.gui!.add(params, "bloomIntensity", 0, 3).name("Bloom");
}
```
+153
View File
@@ -0,0 +1,153 @@
# Best Practices
Generate code that is **simple**, **understandable**, **reviewable**, **scalable**, **optimized**, and **modern**. Follow W3C web standards and platform conventions.
## Naming Conventions
### Files
| Type | Convention | Example |
| ---------- | --------------------------- | -------------------- |
| Components | PascalCase | `WorkshopZone.tsx` |
| Hooks | camelCase with `use` prefix | `useGameState.ts` |
| Managers | PascalCase | `GameManager.ts` |
| Utils | PascalCase | `Dispose.ts` |
| Data | PascalCase | `missions.ts` |
| Shaders | kebab-case | `hologram.vert.glsl` |
### Variables & Functions
| Type | Convention | Example |
| ---------------- | -------------------- | ----------------------------------------- |
| Variables | camelCase | `activeZone`, `missionStep` |
| Functions | camelCase | `startMission()`, `setActiveZone()` |
| Constants | UPPER_SNAKE_CASE | `MAX_SPEED`, `DEFAULT_PHASE` |
| React components | PascalCase | `function WorkshopZone()` |
| React hooks | camelCase with `use` | `useGameState()`, `useFrame()` |
| Classes | PascalCase | `class GameManager` |
| Interfaces/Types | PascalCase | `type GameSnapshot`, `interface ZoneData` |
## Code Style
### Simplicity First
```ts
// Good — clear, direct
function getZoneRadius(zone: Zone): number {
return zone.radius;
}
// Bad — over-abstracted
function getZoneRadius(zone: Zone): number {
return zone[ZoneFields.RADIUS] ?? RADIUS_DEFAULTS[zone.type]?.default ?? 50;
}
```
### Early Return
```ts
// Good
if (!gltf) return null;
if (!visible) return null;
return <primitive object={gltf.scene} />;
```
### Avoid Nested Callbacks
```ts
// Good — flat structure
useEffect(() => {
const mixer = new THREE.AnimationMixer(model);
return () => mixer.stopAllAction();
}, [model]);
```
## TypeScript Rules
### Explicit Types for Exports
```ts
export function useGameState(): GameSnapshot { ... }
export function setPhase(phase: Phase): void { ... }
```
### Never use `any`
```ts
// Good
const ref = useRef<THREE.Group>(null);
// Bad
const ref = useRef<any>(null);
```
## Performance
### useRef for Mutable Values
```ts
// Good — no re-render
const position = useRef(new THREE.Vector3());
// Bad — triggers re-render every frame
const [position, setPosition] = useState(new THREE.Vector3());
```
### Memoize Expensive Computations
```ts
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
```
## Scalability
### Single Responsibility
```ts
// Good — focused component
export function WorkshopZone() {
// Only handles workshop zone logic
}
// Bad — does everything
export function WorkshopZone() {
// Handles zone logic + audio + cinematics + missions
}
```
### Constants for Magic Numbers
```ts
const DEFAULT_CAMERA_DISTANCE = 5;
const ZONE_DETECTION_RADIUS = 20;
```
## Accessibility (W3C)
### Semantic HTML
```tsx
// Good
<button onClick={handleInteract} aria-label="Interact with bike">
<Model />
</button>
// Bad
<div onClick={handleInteract}>
<Model />
</div>
```
## Rules
1. **Simplicity** — Every line of code must be justified.
2. **Readability** — Code is read 10x more than it's written.
3. **Reviewability** — PRs should be understandable in < 5 minutes.
4. **Scalability** — Architecture should support growth without refactoring.
5. **Performance** — Don't optimize prematurely, but don't introduce obvious bottlenecks.
6. **Modern** — Use ES2022+ features, TypeScript strict mode, React hooks.
7. **W3C** — Follow web standards: semantic HTML, ARIA, keyboard navigation.
8. **No Over-Engineering** — Avoid patterns that add complexity without benefit.
+3
View File
@@ -0,0 +1,3 @@
# Features
TODO: Documenter les fonctionnalités du jeu.