upatde: add prettier

This commit is contained in:
2026-04-14 08:59:36 +02:00
parent 25e3d503b2
commit dbb3c46e35
52 changed files with 1444 additions and 268 deletions
+123 -116
View File
@@ -7,33 +7,35 @@ Built with React, Three.js, and Vite. Runs in the browser, no installation requi
## 📦 Tech Stack
### Build & Language
| Package | Doc |
|--------|-----|
| Package | Doc |
| -------------------------------------------------- | ------------------------------------ |
| [TypeScript](https://www.typescriptlang.org/docs/) | https://www.typescriptlang.org/docs/ |
| [React](https://react.dev/learn) | https://react.dev/learn |
| [Vite](https://vite.dev/guide/) | https://vite.dev/guide/ |
| [ESLint](https://eslint.org/docs/latest/) | https://eslint.org/docs/latest/ |
| [Prettier](https://prettier.io/docs/) | https://prettier.io/docs/ |
| [React](https://react.dev/learn) | https://react.dev/learn |
| [Vite](https://vite.dev/guide/) | https://vite.dev/guide/ |
| [ESLint](https://eslint.org/docs/latest/) | https://eslint.org/docs/latest/ |
| [Prettier](https://prettier.io/docs/) | https://prettier.io/docs/ |
### 3D Engine
| Package | Doc |
|--------|-----|
| [Three.js](https://threejs.org/docs/) | https://threejs.org/docs/ |
| [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) | https://docs.pmnd.rs/react-three-fiber |
| [@react-three/drei](https://pmndrs.github.io/drei) | https://pmndrs.github.io/drei |
| [@react-three/rapier](https://rapier.rs/docs/) | https://rapier.rs/docs/user_guides/javascript/ |
| [@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/ |
| Package | Doc |
| ----------------------------------------------------------------------------------------- | ---------------------------------------------- |
| [Three.js](https://threejs.org/docs/) | https://threejs.org/docs/ |
| [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) | https://docs.pmnd.rs/react-three-fiber |
| [@react-three/drei](https://pmndrs.github.io/drei) | https://pmndrs.github.io/drei |
| [@react-three/rapier](https://rapier.rs/docs/) | https://rapier.rs/docs/user_guides/javascript/ |
| [@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/ |
### Performance & Effects
| Package | Doc |
|--------|-----|
| [r3f-perf](https://github.com/utsuboco/r3f-perf) | https://github.com/utsuboco/r3f-perf |
| Package | Doc |
| --------------------------------------------------------------------------- | --------------------------------------------------------- |
| [r3f-perf](https://github.com/utsuboco/r3f-perf) | https://github.com/utsuboco/r3f-perf |
| [AnimationMixer](https://threejs.org/docs/#api/en/animation/AnimationMixer) | https://threejs.org/docs/#api/en/animation/AnimationMixer |
## 🗂 Project Structure
```
la-fabrik/
├── public/
@@ -103,7 +105,7 @@ la-fabrik/
├── App.tsx # Canvas + UI superimposed
└── main.tsx
```
## 🏗 Architecture Patterns
The project uses **two complementary patterns**:
@@ -123,6 +125,7 @@ Consistency matters, but the codebase does **not** force the same lifecycle patt
Only cross-cutting services use the singleton pattern.
Examples:
- `GameManager`
- `CinematicManager`
- `AudioManager`
@@ -135,30 +138,30 @@ These services must exist once, be accessible from anywhere, and coordinate the
```ts
// stateManager/GameManager.ts
export class GameManager {
private static _instance: GameManager | null = null
private static _instance: GameManager | null = null;
cinematic!: CinematicManager
audio!: AudioManager
zone!: ZoneManager
cinematic!: CinematicManager;
audio!: AudioManager;
zone!: ZoneManager;
static getInstance(): GameManager {
if (!GameManager._instance) {
GameManager._instance = new GameManager()
GameManager._instance = new GameManager();
}
return GameManager._instance
return GameManager._instance;
}
private constructor() {
this.cinematic = CinematicManager.getInstance()
this.audio = AudioManager.getInstance()
this.zone = ZoneManager.getInstance()
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
this.cinematic.destroy();
this.audio.destroy();
this.zone.destroy();
GameManager._instance = null;
}
}
```
@@ -166,8 +169,8 @@ export class GameManager {
Usage:
```ts
const game = GameManager.getInstance()
game.startMission('workshop')
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.
@@ -179,6 +182,7 @@ game.startMission('workshop')
All 3D scene objects are implemented as **declarative React components**.
This includes:
- maps
- lights
- environments
@@ -193,34 +197,35 @@ 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'
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)
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)
mixer.current = new THREE.AnimationMixer(gltf.scene);
return () => {
mixer.current?.stopAllAction()
mixer.current = null
}
}, [gltf.scene])
mixer.current?.stopAllAction();
mixer.current = null;
};
}, [gltf.scene]);
useFrame((_, delta) => {
mixer.current?.update(delta)
})
mixer.current?.update(delta);
});
return <primitive ref={root} object={gltf.scene.clone()} />
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
@@ -241,89 +246,90 @@ High-frequency values such as movement, camera interpolation, or physics never g
```ts
// stateManager/GameManager.ts
type Phase = 'loading' | 'intro' | 'exploring' | 'cinematic' | 'outro'
type ZoneId = 'workshop' | 'powerGrid' | 'farm' | null
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
}
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 static _instance: GameManager | null = null;
private listeners = new Set<() => void>();
private state: GameSnapshot = {
phase: 'loading',
phase: "loading",
activeZone: null,
missionId: null,
missionStep: 0,
inputLocked: false,
dialogueId: null,
}
};
static getInstance(): GameManager {
if (!GameManager._instance) {
GameManager._instance = new GameManager()
GameManager._instance = new GameManager();
}
return GameManager._instance
return GameManager._instance;
}
getState(): GameSnapshot {
return this.state
return this.state;
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private emit(): void {
this.listeners.forEach((cb) => cb())
this.listeners.forEach((cb) => cb());
}
setPhase(phase: Phase): void {
this.state.phase = phase
this.emit()
this.state.phase = phase;
this.emit();
}
setActiveZone(zone: ZoneId): void {
this.state.activeZone = zone
this.emit()
this.state.activeZone = zone;
this.emit();
}
startMission(id: string): void {
this.state.missionId = id
this.state.missionStep = 0
this.emit()
this.state.missionId = id;
this.state.missionStep = 0;
this.emit();
}
}
```
```ts
// hooks/useGameState.ts
import { useEffect, useState } from 'react'
import { GameManager } from '@/stateManager/GameManager'
import { useEffect, useState } from "react";
import { GameManager } from "@/stateManager/GameManager";
export function useGameState() {
const game = GameManager.getInstance()
const [state, setState] = useState(game.getState())
const game = GameManager.getInstance();
const [state, setState] = useState(game.getState());
useEffect(() => {
return game.subscribe(() => {
setState({ ...game.getState() })
})
}, [game])
setState({ ...game.getState() });
});
}, [game]);
return state
return state;
}
```
This keeps the architecture simple:
- **GameManager** owns durable gameplay state
- **other managers** handle side effects
- **React components** render that state
@@ -336,6 +342,7 @@ This keeps the architecture simple:
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
@@ -369,6 +376,7 @@ 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
@@ -380,33 +388,33 @@ Shared or cached assets must **not** be blindly disposed.
```ts
// utils/Dispose.ts
import * as THREE from 'three'
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()
value.dispose();
}
}
material.dispose()
material.dispose();
}
static mesh(mesh: THREE.Mesh): void {
mesh.geometry?.dispose()
mesh.geometry?.dispose();
const materials = Array.isArray(mesh.material)
? mesh.material
: [mesh.material]
: [mesh.material];
for (const material of materials) {
if (material) this.material(material)
if (material) this.material(material);
}
}
static renderTarget(rt: THREE.WebGLRenderTarget): void {
rt.texture.dispose()
rt.dispose()
rt.texture.dispose();
rt.dispose();
}
}
```
@@ -418,14 +426,14 @@ useEffect(() => {
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
})
});
meshRef.current.material = material
meshRef.current.material = material;
return () => {
Dispose.material(material)
}
}, [])
Dispose.material(material);
};
}, []);
```
**Rule:** disposal is ownership-based, not automatic and not blind.
@@ -443,29 +451,29 @@ Do not scatter debug checks across the codebase.
```ts
// utils/Debug.ts
import GUI from 'lil-gui'
import GUI from "lil-gui";
export class Debug {
private static _instance: Debug | null = null
private static _instance: Debug | null = null;
readonly active: boolean
gui: GUI | null = null
readonly active: boolean;
gui: GUI | null = null;
static getInstance(): Debug {
if (!Debug._instance) Debug._instance = new Debug()
return Debug._instance
if (!Debug._instance) Debug._instance = new Debug();
return Debug._instance;
}
private constructor() {
this.active = new URLSearchParams(window.location.search).has('debug')
this.active = new URLSearchParams(window.location.search).has("debug");
if (this.active) {
this.gui = new GUI({ title: 'La-Fabrik Debug' })
this.gui = new GUI({ title: "La-Fabrik Debug" });
}
}
destroy(): void {
this.gui?.destroy()
Debug._instance = null
this.gui?.destroy();
Debug._instance = null;
}
}
```
@@ -473,26 +481,25 @@ export class Debug {
Usage:
```ts
const debug = Debug.getInstance()
const debug = Debug.getInstance();
if (debug.active) {
debug.gui!.add(params, 'bloomIntensity', 0, 3).name('Bloom')
debug.gui!.add(params, "bloomIntensity", 0, 3).name("Bloom");
}
```
## 🚀 Getting Started
```bash
git clone https://github.com/La-Fabrik-Durable/La-Fabrik.git
cd La-Fabrik
npm install
npm run dev
```
Open `http://localhost:5173` — standard experience.
Open `http://localhost:5173?debug` — debug panel + r3f-perf overlay.
## 📜 License
See [LICENSE](./LICENSE) file.
See [LICENSE](./LICENSE) file.