diff --git a/.agent/AGENTS.md b/.agent/AGENTS.md index dffc6c3..425e13e 100644 --- a/.agent/AGENTS.md +++ b/.agent/AGENTS.md @@ -80,6 +80,7 @@ import { useGameState } from "@/hooks/useGameState"; 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 - `three.md` — Three.js conventions and AnimationMixer - `gsap.md` — GSAP timeline and cinematic patterns diff --git a/.agent/skills/best-practices.md b/.agent/skills/best-practices.md new file mode 100644 index 0000000..eecf1d0 --- /dev/null +++ b/.agent/skills/best-practices.md @@ -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 ; + +// Bad +if (gltf && visible) { + return ; +} +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(null); + +// Bad +const ref = useRef(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(null); + const gltf = useGLTF("/models/workshop/ebike.glb"); + + useFrame((_, delta) => { + // per-frame logic + }); + + useEffect(() => { + // setup + return () => { + // cleanup + }; + }, []); + + return ; +} +``` + +### 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 + + +// Bad +
+ +
+``` + +### Keyboard Navigation + +```tsx + +``` + +## 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.) diff --git a/README.md b/README.md index 0bec43f..8465be0 100644 --- a/README.md +++ b/README.md @@ -106,388 +106,6 @@ la-fabrik/ └── 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 ``. -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(null); - const gltf = useGLTF("/models/workshop/ebike.glb"); - const mixer = useRef(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 ; -} -``` - -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 ```bash diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md new file mode 100644 index 0000000..bb03d9f --- /dev/null +++ b/docs/technical/architecture.md @@ -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 ``. +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(null); + const gltf = useGLTF("/models/workshop/ebike.glb"); + const mixer = useRef(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 ; +} +``` + +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"); +} +``` diff --git a/docs/technical/best-practices.md b/docs/technical/best-practices.md new file mode 100644 index 0000000..a70fb3e --- /dev/null +++ b/docs/technical/best-practices.md @@ -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 ; +``` + +### 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(null); + +// Bad +const ref = useRef(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 + + +// Bad +
+ +
+``` + +## 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. diff --git a/docs/user/features.md b/docs/user/features.md new file mode 100644 index 0000000..ce78679 --- /dev/null +++ b/docs/user/features.md @@ -0,0 +1,3 @@ +# Features + +TODO: Documenter les fonctionnalités du jeu.