Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e8a68b04a | |||
| b997f576c5 | |||
| 20142b7e5f | |||
| 2b6b045f4a | |||
| 29cd03fc21 | |||
| 7c5d7f3834 | |||
| 93744b15f7 | |||
| 359417ecd4 | |||
| 9ada4298c3 | |||
| 9b8bb1a182 | |||
| a8c6fafbcd | |||
| 14a55e8dd1 | |||
| 2dd5bfeda1 | |||
| e20ead88e1 | |||
| 7e99d455b4 | |||
| 8c6af0ed6d | |||
| 324aa9dc0f | |||
| 356bb5ef88 | |||
| ece9b1268f | |||
| d2735b72a0 | |||
| fc5e4acba4 | |||
| a3db0b2f0d | |||
| f9a0480121 | |||
| aa7db176e6 | |||
| 0f83f57e23 | |||
| d4e7edaa89 | |||
| ab21df18cb | |||
| 7e067ecccd | |||
| 3fac43d5f1 | |||
| abfbb284f5 | |||
| 7588f7f736 | |||
| 5ff5b89302 | |||
| af35150452 | |||
| 31a99902dd | |||
| a259c3d2e2 | |||
| e19cc72ad5 | |||
| e1d2bfdc75 | |||
| 8f40bb8133 | |||
| 7b38f04a0d | |||
| eade051241 | |||
| e868e72402 | |||
| 4783784fb3 | |||
| bfe8c49323 | |||
| 1a91fcaca0 | |||
| 055e7b2e63 | |||
| 21d91f1de1 | |||
| 68b0ceb593 | |||
| 868f7a1cfd | |||
| 7fd39f58d8 | |||
| 2001955625 | |||
| 3254291ba7 | |||
| 753a767662 | |||
| bcf3a63fc5 | |||
| ab8c84e006 | |||
| 8abc69ebc3 | |||
| b63412de13 | |||
| 9fdf065c1d | |||
| 4a697ab790 | |||
| 29144f8844 | |||
| 74b9bf57c8 | |||
| 5402c343fa | |||
| 5569da07c1 | |||
| 38abeb3b49 | |||
| eb0db21d29 | |||
| 393b653cca | |||
| e87004652f | |||
| 8c84663472 | |||
| 6b8ba3d58d | |||
| d0cf876372 | |||
| 38f9f087d1 | |||
| dcbc1c73f5 | |||
| f9c4495610 | |||
| 638022339e | |||
| 20fbaf05e1 | |||
| ed7681a293 | |||
| b26da614f0 | |||
| 1eed905e8b | |||
| 7769959135 | |||
| fd7571fbe1 | |||
| 3506858c96 | |||
| 61d7495ec9 | |||
| d486f6f381 | |||
| f67799db30 |
@@ -0,0 +1,55 @@
|
|||||||
|
# Agent - La Fabrik
|
||||||
|
|
||||||
|
You are working on **La Fabrik**, an interactive 3D web experience built with React Three Fiber.
|
||||||
|
|
||||||
|
## Read This First
|
||||||
|
|
||||||
|
- `docs/technical/architecture.md` describes the code that exists today.
|
||||||
|
- `docs/technical/target-architecture.md` describes the intended target-state.
|
||||||
|
- Do not assume target-state systems already exist.
|
||||||
|
|
||||||
|
## Current Implementation
|
||||||
|
|
||||||
|
- Stack: React 19, Three.js, `@react-three/fiber`, `@react-three/drei`, `@react-three/rapier`, TypeScript, Vite
|
||||||
|
- No external global state library is used.
|
||||||
|
- Current singleton-style services are limited to:
|
||||||
|
- `InteractionManager`
|
||||||
|
- `AudioManager`
|
||||||
|
- `Debug`
|
||||||
|
- Current gameplay scope is still prototype-level:
|
||||||
|
- player movement
|
||||||
|
- trigger/grab interactions
|
||||||
|
- debug camera and scene switching
|
||||||
|
- simple audio playback
|
||||||
|
|
||||||
|
## Current Architecture Rules
|
||||||
|
|
||||||
|
- Scene objects live in `src/world/` and `src/components/3d/`.
|
||||||
|
- HTML overlays live in `src/components/ui/`.
|
||||||
|
- Shared static config lives in `src/data/`.
|
||||||
|
- Debug tooling lives in `src/utils/debug/` and `src/hooks/debug/`.
|
||||||
|
- Use the `@/` alias for imports from `src/`.
|
||||||
|
- Prefer small, direct changes over adding new abstraction layers.
|
||||||
|
- Shared types should live close to their domain and only move outward when they gain multiple real consumers.
|
||||||
|
|
||||||
|
## Target-State Guidance
|
||||||
|
|
||||||
|
The project may later grow toward a manager-driven gameplay architecture with clearer separation between:
|
||||||
|
|
||||||
|
- production world code
|
||||||
|
- gameplay orchestration
|
||||||
|
- UI overlays
|
||||||
|
- debug tooling
|
||||||
|
|
||||||
|
That target-state is aspirational until the matching code exists. If a target-state rule conflicts with the current implementation, treat the current code as the source of truth and improve it incrementally.
|
||||||
|
|
||||||
|
## Do Not Assume
|
||||||
|
|
||||||
|
- There is no `GameManager` in the current codebase.
|
||||||
|
- There are no implemented mission, zone, cinematic, or dialogue systems yet.
|
||||||
|
- Dependency versions are not pinned today; do not rewrite dependency strategy unless explicitly asked.
|
||||||
|
- The old `# route path ...` file header convention is not in use.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
Files in `.agent/skills/` are supplemental patterns and examples. Some describe target-state or generic practices rather than the exact current implementation, so verify against the code before applying them.
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# Agent — La Fabrik
|
|
||||||
|
|
||||||
You are working on **La Fabrik**, an interactive 3D web experience built with React Three Fiber. The player steps into the role of a technician in Altera (2050) and completes missions: repairing an e-bike, fixing a power grid, upgrading a vertical farm.
|
|
||||||
|
|
||||||
## Project Identity
|
|
||||||
|
|
||||||
- **Stack:** React 19, Three.js, @react-three/fiber 9, @react-three/drei, @react-three/rapier, GSAP, TypeScript, Vite
|
|
||||||
- **No external state lib.** State is managed by a custom `GameManager` singleton with a subscribe/getState pattern.
|
|
||||||
- **No Zustand, no Redux, no Context for global state.**
|
|
||||||
- **Versions are pinned** (no `^` in dependencies). Do not upgrade packages without explicit request.
|
|
||||||
|
|
||||||
## Architecture Rules
|
|
||||||
|
|
||||||
### Two patterns coexist
|
|
||||||
|
|
||||||
1. **Singleton manager classes** — for orchestration, audio, cinematics, zone detection, debug
|
|
||||||
2. **Declarative React components** — for all 3D scene objects (map, zones, lights, player, postprocessing)
|
|
||||||
|
|
||||||
Scene objects are **never** singleton classes. Managers are **never** React components.
|
|
||||||
|
|
||||||
### State ownership
|
|
||||||
|
|
||||||
- `GameManager` is the single source of truth for durable gameplay state (phase, zone, mission, input lock, dialogue)
|
|
||||||
- Other managers (`CinematicManager`, `AudioManager`, `ZoneManager`) handle side effects only — they read from GameManager but do not duplicate its state
|
|
||||||
- React components subscribe to GameManager through `useGameState()` hook
|
|
||||||
- **High-frequency values** (movement, camera interpolation, physics) stay in `useRef` + `useFrame` — never in React state
|
|
||||||
|
|
||||||
### File conventions
|
|
||||||
|
|
||||||
- Every file starts with a comment: `# route path <relative_path>` (e.g. `# route path src/world/Map.tsx`)
|
|
||||||
- Scene components live in `src/world/` and `src/components/3d/`
|
|
||||||
- UI overlays live in `src/components/ui/`
|
|
||||||
- Managers live in `src/stateManager/`
|
|
||||||
- Hooks live in `src/hooks/`
|
|
||||||
- Static data lives in `src/data/`
|
|
||||||
- Shaders live in `src/shaders/`
|
|
||||||
- Utilities live in `src/utils/`
|
|
||||||
|
|
||||||
### Import paths
|
|
||||||
|
|
||||||
Use `@/` alias for imports from `src/`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { GameManager } from "@/stateManager/GameManager";
|
|
||||||
import { useGameState } from "@/hooks/useGameState";
|
|
||||||
```
|
|
||||||
|
|
||||||
### Memory management
|
|
||||||
|
|
||||||
- Dispose only what you own (custom materials, render targets, manual clones)
|
|
||||||
- Never blindly deep-dispose shared/cached assets (drei loaders cache models)
|
|
||||||
- Use `Dispose.material()`, `Dispose.mesh()`, `Dispose.renderTarget()` from `src/utils/Dispose.ts`
|
|
||||||
|
|
||||||
### Debug
|
|
||||||
|
|
||||||
- Debug panel activates with `?debug` in URL
|
|
||||||
- All debug logic goes through `Debug.getInstance()` from `src/utils/Debug.ts`
|
|
||||||
- Never scatter `if (isDev)` blocks across files
|
|
||||||
- `r3f-perf` is lazy-loaded only in debug mode via `src/components/3d/DebugPerf.tsx`
|
|
||||||
|
|
||||||
## Managers (4 max)
|
|
||||||
|
|
||||||
| Manager | Responsibility |
|
|
||||||
| ------------------ | ------------------------------------------------------------------- |
|
|
||||||
| `GameManager` | Phase, zone, mission, input lock, dialogue — single source of truth |
|
|
||||||
| `CinematicManager` | GSAP timelines, camera lock/unlock |
|
|
||||||
| `AudioManager` | Music, SFX, spatial audio |
|
|
||||||
| `ZoneManager` | Zone detection, LOD triggers |
|
|
||||||
|
|
||||||
## Do NOT
|
|
||||||
|
|
||||||
- Create new manager classes without explicit request
|
|
||||||
- Use Zustand, Redux, or React Context for global state
|
|
||||||
- Put high-frequency values in React state (`useState`)
|
|
||||||
- Import `CinematicManager`/`AudioManager`/`ZoneManager` directly from components — always go through `GameManager`
|
|
||||||
- Upgrade pinned dependency versions
|
|
||||||
- Create files outside the documented architecture without explicit request
|
|
||||||
|
|
||||||
## Skills
|
|
||||||
|
|
||||||
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
|
|
||||||
- `managers.md` — Singleton manager implementation
|
|
||||||
- `memory.md` — GPU memory and disposal rules
|
|
||||||
- `debug.md` — Debug utility and r3f-perf setup
|
|
||||||
@@ -8,10 +8,12 @@ Append `?debug` to the URL:
|
|||||||
http://localhost:5173?debug
|
http://localhost:5173?debug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The free debug camera is toggled from the debug panel, not mounted permanently.
|
||||||
|
|
||||||
## Debug singleton
|
## Debug singleton
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// src/utils/Debug.ts
|
// src/utils/debug/Debug.ts
|
||||||
import GUI from "lil-gui";
|
import GUI from "lil-gui";
|
||||||
|
|
||||||
export class Debug {
|
export class Debug {
|
||||||
@@ -56,14 +58,15 @@ if (debug.active) {
|
|||||||
r3f-perf is loaded only in debug mode to avoid dependency issues in production:
|
r3f-perf is loaded only in debug mode to avoid dependency issues in production:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// src/components/3d/DebugPerf.tsx
|
// src/utils/debug/DebugPerf.tsx
|
||||||
import { Suspense, lazy } from "react";
|
import { Suspense, lazy } from "react";
|
||||||
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
const Perf = lazy(() => import("r3f-perf").then((m) => ({ default: m.Perf })));
|
const Perf = lazy(() => import("r3f-perf").then((m) => ({ default: m.Perf })));
|
||||||
|
|
||||||
export function DebugPerf() {
|
export function DebugPerf() {
|
||||||
const debug = new URLSearchParams(window.location.search).has("debug");
|
const debug = Debug.getInstance();
|
||||||
if (!debug) return null;
|
if (!debug.active) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ name: 🔍 Lint
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
|
branches: [develop, main]
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@@ -71,6 +75,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: ⬇️ Checkout
|
- name: ⬇️ Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
|
||||||
- name: 🧰 Setup Node
|
- name: 🧰 Setup Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ name: 📊 Quality
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
|
branches: [develop, main]
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security:
|
security:
|
||||||
@@ -53,6 +57,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: ⬇️ Checkout
|
- name: ⬇️ Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
|
||||||
- name: 🧰 Setup Node
|
- name: 🧰 Setup Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
@@ -68,12 +74,12 @@ jobs:
|
|||||||
|
|
||||||
- name: 📏 Check bundle size
|
- name: 📏 Check bundle size
|
||||||
run: |
|
run: |
|
||||||
# Get bundle size in KB
|
# Check generated app assets only; public/ model files are runtime assets copied to dist.
|
||||||
SIZE=$(du -k dist | cut -f1)
|
SIZE=$(du -k dist/assets | cut -f1)
|
||||||
echo "Bundle size: ${SIZE}KB"
|
echo "Bundle size: ${SIZE}KB"
|
||||||
|
|
||||||
# Threshold: 1000KB (configurable)
|
# Threshold: 5000KB (configurable)
|
||||||
THRESHOLD=1000
|
THRESHOLD=5000
|
||||||
|
|
||||||
if [ "$SIZE" -gt "$THRESHOLD" ]; then
|
if [ "$SIZE" -gt "$THRESHOLD" ]; then
|
||||||
echo "❌ Bundle size ${SIZE}KB exceeds threshold ${THRESHOLD}KB"
|
echo "❌ Bundle size ${SIZE}KB exceeds threshold ${THRESHOLD}KB"
|
||||||
|
|||||||
@@ -38,3 +38,8 @@ Thumbs.db
|
|||||||
# 3D Assets Cache (drei, GLTFJSX)
|
# 3D Assets Cache (drei, GLTFJSX)
|
||||||
.drei/
|
.drei/
|
||||||
.glitchdrei-cache/
|
.glitchdrei-cache/
|
||||||
|
|
||||||
|
# Temporaire
|
||||||
|
.backend/
|
||||||
|
backend/
|
||||||
|
temp/
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
public/models
|
||||||
|
public/**/*.glb
|
||||||
|
public/**/*.gltf
|
||||||
|
public/**/*.png
|
||||||
|
public/**/*.jpg
|
||||||
|
public/**/*.jpeg
|
||||||
|
public/**/*.webp
|
||||||
|
public/**/*.hdr
|
||||||
|
public/**/*.exr
|
||||||
|
public/**/*.ktx
|
||||||
|
public/**/*.ktx2
|
||||||
|
public/**/*.mp3
|
||||||
|
public/**/*.wav
|
||||||
|
public/**/*.ogg
|
||||||
|
public/**/*.mp4
|
||||||
|
public/**/*.webm
|
||||||
@@ -8,31 +8,31 @@ Built with React, Three.js, and Vite. Runs in the browser, no installation requi
|
|||||||
|
|
||||||
### Build & Language
|
### Build & Language
|
||||||
|
|
||||||
| Package | Doc |
|
| Package |
|
||||||
| -------------------------------------------------- | ------------------------------------ |
|
| -------------------------------------------------- |
|
||||||
| [TypeScript](https://www.typescriptlang.org/docs/) | https://www.typescriptlang.org/docs/ |
|
| [TypeScript](https://www.typescriptlang.org/docs/) |
|
||||||
| [React](https://react.dev/learn) | https://react.dev/learn |
|
| [React](https://react.dev/learn) |
|
||||||
| [Vite](https://vite.dev/guide/) | https://vite.dev/guide/ |
|
| [Vite](https://vite.dev/guide/) |
|
||||||
| [ESLint](https://eslint.org/docs/latest/) | https://eslint.org/docs/latest/ |
|
| [ESLint](https://eslint.org/docs/latest/) |
|
||||||
| [Prettier](https://prettier.io/docs/) | https://prettier.io/docs/ |
|
| [Prettier](https://prettier.io/docs/) |
|
||||||
|
|
||||||
### 3D Engine
|
### 3D Engine
|
||||||
|
|
||||||
| Package | Doc |
|
| Package |
|
||||||
| ----------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
| ----------------------------------------------------------------------------------------- |
|
||||||
| [Three.js](https://threejs.org/docs/) | https://threejs.org/docs/ |
|
| [Three.js](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/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) |
|
||||||
| [@react-three/drei](https://pmndrs.github.io/drei) | https://pmndrs.github.io/drei |
|
| [@react-three/drei](https://pmndrs.github.io/drei) |
|
||||||
| [@react-three/rapier](https://rapier.rs/docs/) | https://rapier.rs/docs/user_guides/javascript/ |
|
| [@react-three/rapier](https://rapier.rs/docs/) |
|
||||||
| [@react-three/postprocessing](https://github.com/pmndrs/postprocessing) | https://github.com/pmndrs/postprocessing |
|
| [@react-three/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/) |
|
||||||
|
|
||||||
### Performance & Effects
|
### Performance & Effects
|
||||||
|
|
||||||
| Package | Doc |
|
| Package |
|
||||||
| --------------------------------------------------------------------------- | --------------------------------------------------------- |
|
| --------------------------------------------------------------------------- |
|
||||||
| [r3f-perf](https://github.com/utsuboco/r3f-perf) | https://github.com/utsuboco/r3f-perf |
|
| [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 |
|
| [AnimationMixer](https://threejs.org/docs/#api/en/animation/AnimationMixer) |
|
||||||
|
|
||||||
## 🗂 Project Structure
|
## 🗂 Project Structure
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ la-fabrik/
|
|||||||
│
|
│
|
||||||
└── src/
|
└── src/
|
||||||
├── world/ # Single persistent 3D world
|
├── world/ # Single persistent 3D world
|
||||||
|
│ ├── World.tsx # Main scene composition
|
||||||
│ ├── Map.tsx # Base map, always mounted
|
│ ├── Map.tsx # Base map, always mounted
|
||||||
│ ├── Lighting.tsx # Ambient, directional, point lights
|
│ ├── Lighting.tsx # Ambient, directional, point lights
|
||||||
│ ├── Environment.tsx # HDRI, fog, sky
|
│ ├── Environment.tsx # HDRI, fog, sky
|
||||||
@@ -98,11 +99,24 @@ la-fabrik/
|
|||||||
│ └── fragment.glsl
|
│ └── fragment.glsl
|
||||||
│
|
│
|
||||||
├── utils/
|
├── utils/
|
||||||
│ ├── Debug.ts # lil-gui panel
|
│ ├── EventEmitter.ts # Simple typed pub/sub utility
|
||||||
│ ├── EventEmitter.ts # Simple pub/sub for manager-to-manager events
|
│ ├── Sizes.ts # Viewport size tracking
|
||||||
│ └── Dispose.ts # traverse() + dispose() helper
|
│ ├── Time.ts # Animation frame timing utility
|
||||||
|
│ └── debug/ # Dev-only tools and scene inspection
|
||||||
|
│ ├── Debug.ts # Global lil-gui manager
|
||||||
|
│ ├── DebugPerf.tsx # r3f-perf overlay mounted in Canvas
|
||||||
|
│ ├── isDebugEnabled.ts # Debug query-string helper
|
||||||
|
│ └── scene/
|
||||||
|
│ ├── DebugHelpers.tsx # Grid + axes helpers shown in debug mode
|
||||||
|
│ └── DebugCameraControls.tsx # Free debug camera for map inspection
|
||||||
|
├── hooks/
|
||||||
|
│ └── debug/
|
||||||
|
│ ├── useCameraMode.ts
|
||||||
|
│ ├── useDebugFolder.ts
|
||||||
|
│ ├── useDebugStore.ts
|
||||||
|
│ └── useSceneMode.ts
|
||||||
│
|
│
|
||||||
├── App.tsx # Canvas + UI superimposed
|
├── App.tsx # Canvas bootstrap
|
||||||
└── main.tsx
|
└── main.tsx
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -115,8 +129,8 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://localhost:5173` — standard experience.
|
- app: `http://localhost:5173`
|
||||||
Open `http://localhost:5173?debug` — debug panel + r3f-perf overlay.
|
- debug mode: `http://localhost:5173?debug`
|
||||||
|
|
||||||
## 📜 License
|
## 📜 License
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,338 @@
|
|||||||
|
# Animation & 3D Model System
|
||||||
|
|
||||||
|
This document describes how to use the 3D model components and animation system in La-Fabrik.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Model Types Overview](#model-types-overview)
|
||||||
|
2. [SimpleModel - Static Models](#simplemodel---static-models)
|
||||||
|
3. [AnimatedModel - Animated Models](#animatedmodel---animated-models)
|
||||||
|
4. [Animation Control](#animation-control)
|
||||||
|
5. [Other 3D Components](#other-3d-components)
|
||||||
|
6. [Technical Notes](#technical-notes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Model Types Overview
|
||||||
|
|
||||||
|
The project provides three main types of model instantiation:
|
||||||
|
|
||||||
|
| Type | Component | Use Case |
|
||||||
|
| ----------- | -------------------------------------------------------- | -------------------------------------------- |
|
||||||
|
| Static | `SimpleModel` | Props, decoration, objects without animation |
|
||||||
|
| Animated | `AnimatedModel` | Characters, animated objects with skeleton |
|
||||||
|
| Interactive | `GrabbableObject`, `TriggerObject`, `InteractableObject` | Objects player can interact with |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SimpleModel - Static Models
|
||||||
|
|
||||||
|
Use for GLTF models **without** skeleton/armature and no animations.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { SimpleModel } from "@/components/3d";
|
||||||
|
|
||||||
|
<SimpleModel
|
||||||
|
modelPath="/models/elecsimple/model.gltf"
|
||||||
|
position={[0, 0, -5]}
|
||||||
|
rotation={[0, 45, 0]}
|
||||||
|
scale={1}
|
||||||
|
castShadow={true}
|
||||||
|
receiveShadow={true}
|
||||||
|
/>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| Prop | Type | Default | Description |
|
||||||
|
| --------------- | ------------------------ | ----------- | --------------------------------- |
|
||||||
|
| `modelPath` | `string` | required | Path to GLTF file in `/public` |
|
||||||
|
| `position` | `Vector3Tuple` | `[0, 0, 0]` | World position [x, y, z] |
|
||||||
|
| `rotation` | `Vector3Tuple` | `[0, 0, 0]` | Rotation in degrees [x, y, z] |
|
||||||
|
| `scale` | `number \| Vector3Tuple` | `1` | Scale factor or [x, y, z] |
|
||||||
|
| `castShadow` | `boolean` | `true` | Enable shadow casting |
|
||||||
|
| `receiveShadow` | `boolean` | `true` | Enable shadow receiving |
|
||||||
|
| `children` | `ReactNode` | - | Child components to render inside |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AnimatedModel - Animated Models
|
||||||
|
|
||||||
|
Use for GLTF models **with** skeleton/armature and animations (like Mixamo characters).
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { AnimatedModel, useAnimatedModel } from "@/components/3d";
|
||||||
|
|
||||||
|
// Basic usage
|
||||||
|
<AnimatedModel
|
||||||
|
modelPath="/models/elec/model.gltf"
|
||||||
|
defaultAnimation="Idle"
|
||||||
|
position={[0, 0, -5]}
|
||||||
|
rotation={[0, 0, 0]}
|
||||||
|
scale={0.01}
|
||||||
|
autoPlay={true}
|
||||||
|
speed={1}
|
||||||
|
fadeDuration={0.3}
|
||||||
|
/>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| Prop | Type | Default | Description |
|
||||||
|
| ------------------ | ------------------------ | ----------- | --------------------------------------------- |
|
||||||
|
| `modelPath` | `string` | required | Path to GLTF file in `/public` |
|
||||||
|
| `defaultAnimation` | `string` | `"Idle"` | Animation name to play by default |
|
||||||
|
| `animations` | `string[]` | `[]` | List of animation names (optional) |
|
||||||
|
| `position` | `Vector3Tuple` | `[0, 0, 0]` | World position [x, y, z] |
|
||||||
|
| `rotation` | `Vector3Tuple` | `[0, 0, 0]` | Rotation in degrees [x, y, z] |
|
||||||
|
| `scale` | `number \| Vector3Tuple` | `1` | Scale factor |
|
||||||
|
| `autoPlay` | `boolean` | `true` | Auto-play default animation |
|
||||||
|
| `speed` | `number` | `1` | Animation playback speed |
|
||||||
|
| `fadeDuration` | `number` | `0.3` | Transition duration in seconds |
|
||||||
|
| `onLoaded` | `() => void` | - | Callback when model loads |
|
||||||
|
| `onAnimationEnd` | `(name: string) => void` | - | Callback when animation ends |
|
||||||
|
| `children` | `ReactNode` | - | Child components (can use `useAnimatedModel`) |
|
||||||
|
|
||||||
|
### Important: Scale
|
||||||
|
|
||||||
|
Animated models (like Mixamo exports) often need a small scale (e.g., `0.01`) because they are exported in meters while Three.js uses different units. Adjust until the model appears at the right size.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Animation Control
|
||||||
|
|
||||||
|
To control animations from inside or outside the `AnimatedModel`, use the `useAnimatedModel` hook.
|
||||||
|
|
||||||
|
### Basic Control
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { AnimatedModel, useAnimatedModel } from "@/components/3d";
|
||||||
|
|
||||||
|
// Create a controller component to use inside AnimatedModel
|
||||||
|
function AnimationController() {
|
||||||
|
const { play, stop, fadeTo, currentAnimation, names, setSpeed, isReady } =
|
||||||
|
useAnimatedModel();
|
||||||
|
|
||||||
|
// names contains all available animation names
|
||||||
|
// currentAnimation is the name of the currently playing animation
|
||||||
|
// isReady is true when model and animations are loaded
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh onClick={() => play("Run", 0.5)}>
|
||||||
|
<boxGeometry />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<AnimatedModel
|
||||||
|
modelPath="/models/elec/model.gltf"
|
||||||
|
defaultAnimation="Idle"
|
||||||
|
position={[0, 0, -5]}
|
||||||
|
scale={0.01}
|
||||||
|
>
|
||||||
|
<AnimationController />
|
||||||
|
</AnimatedModel>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Methods
|
||||||
|
|
||||||
|
| Method | Signature | Description |
|
||||||
|
| ------------------ | --------------------------------------- | ------------------------------------ |
|
||||||
|
| `play` | `(name: string, fade?: number) => void` | Play animation with optional fade |
|
||||||
|
| `fadeTo` | `(name: string, fade?: number) => void` | Fade to another animation |
|
||||||
|
| `stop` | `(fade?: number) => void` | Stop and return to default animation |
|
||||||
|
| `setSpeed` | `(speed: number) => void` | Set animation speed |
|
||||||
|
| `currentAnimation` | `string` | Current animation name (getter) |
|
||||||
|
| `names` | `string[]` | Available animation names |
|
||||||
|
| `isReady` | `boolean` | Whether model is loaded |
|
||||||
|
|
||||||
|
### Transition Example
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Character() {
|
||||||
|
const { play, fadeTo, currentAnimation } = useAnimatedModel();
|
||||||
|
|
||||||
|
const handleWalk = () => fadeTo("Walk", 0.5); // 0.5s fade
|
||||||
|
const handleRun = () => play("Run", 0.3); // 0.3s fade
|
||||||
|
const handleIdle = () => play("Idle", 0.5); // return to idle
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
<mesh onClick={handleWalk} position={[-1, 0, 0]}>
|
||||||
|
<boxGeometry />
|
||||||
|
</mesh>
|
||||||
|
<mesh onClick={handleRun} position={[0, 0, 0]}>
|
||||||
|
<boxGeometry />
|
||||||
|
</mesh>
|
||||||
|
<mesh onClick={handleIdle} position={[1, 0, 0]}>
|
||||||
|
<boxGeometry />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combined: GrabbableObject with Animation
|
||||||
|
|
||||||
|
You can combine `AnimatedModel` inside `GrabbableObject` to create animated objects that can be picked up:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { AnimatedModel, GrabbableObject } from "@/components/3d";
|
||||||
|
|
||||||
|
// Animated weapon/tool that player can pick up
|
||||||
|
<GrabbableObject position={[0, 1, 0]} colliders="cuboid">
|
||||||
|
<AnimatedModel
|
||||||
|
modelPath="/models/sword/model.gltf"
|
||||||
|
defaultAnimation="Idle"
|
||||||
|
position={[0, 0, 0]}
|
||||||
|
scale={0.02}
|
||||||
|
autoPlay={true}
|
||||||
|
/>
|
||||||
|
</GrabbableObject>;
|
||||||
|
```
|
||||||
|
|
||||||
|
Or create an animated character that can be grabbed:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {
|
||||||
|
AnimatedModel,
|
||||||
|
GrabbableObject,
|
||||||
|
useAnimatedModel,
|
||||||
|
} from "@/components/3d";
|
||||||
|
|
||||||
|
// Controller that triggers animations when grabbed
|
||||||
|
function AnimatedGrabber() {
|
||||||
|
const { play, fadeTo } = useAnimatedModel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedModel
|
||||||
|
modelPath="/models/elec/model.gltf"
|
||||||
|
defaultAnimation="Idle"
|
||||||
|
position={[0, 0, 0]}
|
||||||
|
scale={0.01}
|
||||||
|
autoPlay={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When grabbed, play "Grab" animation
|
||||||
|
<GrabbableObject
|
||||||
|
position={[0, 1, 0]}
|
||||||
|
colliders="cuboid"
|
||||||
|
onGrab={() => {
|
||||||
|
// This would require a context or store to trigger
|
||||||
|
console.log("Object grabbed!");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatedGrabber />
|
||||||
|
</GrabbableObject>;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** For complex interactions (like playing specific animations when grabbing), you'll need to connect the grab events to animation controls via a state manager or context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Other 3D Components
|
||||||
|
|
||||||
|
### GrabbableObject
|
||||||
|
|
||||||
|
Objects that can be picked up by the player.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { GrabbableObject } from "@/components/3d";
|
||||||
|
|
||||||
|
<GrabbableObject position={[0, 1, 0]} colliders="cuboid">
|
||||||
|
<mesh>
|
||||||
|
<boxGeometry args={[0.5, 0.5, 0.5]} />
|
||||||
|
<meshStandardMaterial color="red" />
|
||||||
|
</mesh>
|
||||||
|
</GrabbableObject>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### TriggerObject
|
||||||
|
|
||||||
|
Objects that trigger events when interacted with.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { TriggerObject } from "@/components/3d";
|
||||||
|
|
||||||
|
<TriggerObject
|
||||||
|
position={[0, 1, 0]}
|
||||||
|
soundPath="/sounds/click.mp3"
|
||||||
|
onTrigger={() => console.log("Triggered!")}
|
||||||
|
>
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry />
|
||||||
|
<meshStandardMaterial color="blue" />
|
||||||
|
</mesh>
|
||||||
|
</TriggerObject>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### InteractableObject
|
||||||
|
|
||||||
|
Base object for interactions.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { InteractableObject } from "@/components/3d";
|
||||||
|
|
||||||
|
<InteractableObject
|
||||||
|
position={[0, 1, 0]}
|
||||||
|
onInteract={() => console.log("Interacted!")}
|
||||||
|
>
|
||||||
|
<mesh>
|
||||||
|
<cylinderGeometry />
|
||||||
|
<meshStandardMaterial color="green" />
|
||||||
|
</mesh>
|
||||||
|
</InteractableObject>;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
|
||||||
|
### GLTF Models
|
||||||
|
|
||||||
|
- Models should be placed in `/public/models/`
|
||||||
|
- Supported formats: `.gltf`, `.glb`
|
||||||
|
- Animated models must have an Armature/skeleton for animations to work
|
||||||
|
|
||||||
|
### Model Scale Issue
|
||||||
|
|
||||||
|
If animated models don't appear, they may be too small or too large. Try:
|
||||||
|
|
||||||
|
- Scale `0.01` for Mixamo-exported models
|
||||||
|
- Scale `1` for models in correct units
|
||||||
|
|
||||||
|
### Cloning
|
||||||
|
|
||||||
|
- `SimpleModel` uses `scene.clone()` for proper React lifecycle
|
||||||
|
- `AnimatedModel` uses the original scene directly to preserve SkinnedMesh + Armature structure
|
||||||
|
|
||||||
|
### Animation System
|
||||||
|
|
||||||
|
The animation system uses:
|
||||||
|
|
||||||
|
- `@react-three/drei`: `useGLTF` for loading, `useAnimations` for animation control
|
||||||
|
- Three.js: `AnimationMixer` for playback
|
||||||
|
|
||||||
|
### No State Machine
|
||||||
|
|
||||||
|
This system intentionally avoids complex state machines (like Unity's Animator). For simple animation transitions, use the `play`, `fadeTo`, and `stop` methods directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/3d/
|
||||||
|
│ ├── AnimatedModel.tsx # Animated model component + context
|
||||||
|
│ ├── SimpleModel.tsx # Static model component
|
||||||
|
│ ├── GrabbableObject.tsx # Pickable object
|
||||||
|
│ ├── TriggerObject.tsx # Trigger event object
|
||||||
|
│ ├── InteractableObject.tsx
|
||||||
|
│ └── index.ts # Central exports
|
||||||
|
└── hooks/
|
||||||
|
└── useCharacterAnimation.ts # Animation hook (legacy)
|
||||||
|
```
|
||||||
+71
-381
@@ -1,381 +1,71 @@
|
|||||||
# Architecture Patterns
|
# Current Architecture
|
||||||
|
|
||||||
The project uses **two complementary patterns**:
|
This document describes the code that exists today in the repository.
|
||||||
|
|
||||||
- **Singleton service classes** for orchestration and side effects
|
## Runtime Structure
|
||||||
- **Declarative React components** for all 3D scene objects
|
|
||||||
|
- `src/main.tsx` mounts React and wraps the app in `BrowserRouter`.
|
||||||
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>`.
|
- `src/App.tsx` declares the top-level routes:
|
||||||
Global systems such as gameplay flow, cinematics, audio, and debug tooling are implemented as **manager classes**.
|
- `/` mounts the playable 3D scene, debug perf overlay, and HTML overlays.
|
||||||
|
- `/editor` mounts the map editor page.
|
||||||
Consistency matters, but the codebase does **not** force the same lifecycle pattern on scene components and global services.
|
- `src/world/World.tsx` composes the active scene, including:
|
||||||
|
- environment and lighting
|
||||||
---
|
- debug helpers and debug camera mode
|
||||||
|
- either the map scene or the debug physics test scene
|
||||||
## 1. Singleton Pattern for Global Managers Only
|
- the player rig when the active camera mode is `player`
|
||||||
|
- `src/world/GameMap.tsx` loads map nodes from `public/map.json`, resolves available models, and builds the collision octree.
|
||||||
Only cross-cutting services use the singleton pattern.
|
- `src/world/debug/TestScene.tsx` provides a debug-oriented interaction and physics scene.
|
||||||
|
- `src/world/player/PlayerComponent.tsx` mounts the camera and controller.
|
||||||
Examples:
|
- `src/world/player/PlayerController.tsx` owns pointer lock movement, jump handling, and interaction input.
|
||||||
|
|
||||||
- `GameManager`
|
## Interaction Model
|
||||||
- `CinematicManager`
|
|
||||||
- `AudioManager`
|
- `src/stateManager/InteractionManager.ts` is the current interaction state source.
|
||||||
- `ZoneManager`
|
- `src/components/3d/InteractableObject.tsx` handles focus detection through distance and raycasting.
|
||||||
- `Debug`
|
- `src/components/3d/TriggerObject.tsx` implements trigger-style interactions.
|
||||||
- `EventEmitter`
|
- `src/components/3d/GrabbableObject.tsx` implements hold-and-release interactions.
|
||||||
|
- `src/hooks/useInteraction.ts` exposes the interaction snapshot to React UI.
|
||||||
These services must exist once, be accessible from anywhere, and coordinate the experience globally.
|
- `src/components/ui/InteractPrompt.tsx` shows the `E` prompt for trigger interactions.
|
||||||
|
|
||||||
```ts
|
## Audio
|
||||||
// stateManager/GameManager.ts
|
|
||||||
export class GameManager {
|
- `src/stateManager/AudioManager.ts` currently provides pooled one-shot sound playback.
|
||||||
private static _instance: GameManager | null = null;
|
- Trigger interactions may play audio directly through `AudioManager`.
|
||||||
|
|
||||||
cinematic!: CinematicManager;
|
## Debug System
|
||||||
audio!: AudioManager;
|
|
||||||
zone!: ZoneManager;
|
- Debug mode is enabled with `?debug`.
|
||||||
|
- `src/utils/debug/Debug.ts` owns the `lil-gui` instance and debug controls.
|
||||||
static getInstance(): GameManager {
|
- `src/hooks/debug/useCameraMode.ts` and `src/hooks/debug/useSceneMode.ts` subscribe to debug state.
|
||||||
if (!GameManager._instance) {
|
- `src/utils/debug/DebugPerf.tsx` lazily mounts `r3f-perf` in debug mode.
|
||||||
GameManager._instance = new GameManager();
|
- `src/utils/debug/scene/DebugHelpers.tsx` mounts debug helpers.
|
||||||
}
|
- `src/utils/debug/scene/DebugCameraControls.tsx` mounts the free debug camera.
|
||||||
return GameManager._instance;
|
|
||||||
}
|
## Editor System
|
||||||
|
|
||||||
private constructor() {
|
- `src/pages/editor/EditorPage.tsx` is the route-level editor page for `/editor`.
|
||||||
this.cinematic = CinematicManager.getInstance();
|
- `src/components/editor/EditorControls.tsx` renders the HTML editor control panel.
|
||||||
this.audio = AudioManager.getInstance();
|
- `src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, shortcuts, and map rendering.
|
||||||
this.zone = ZoneManager.getInstance();
|
- `src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||||
}
|
- `src/controls/editor/FlyController.tsx` provides player-style editor navigation.
|
||||||
|
- `src/hooks/editor/useEditorSceneData.ts` loads scene data and handles folder upload fallback.
|
||||||
destroy(): void {
|
- `src/hooks/editor/useEditorHistory.ts` owns editor undo and redo state.
|
||||||
this.cinematic.destroy();
|
- `src/utils/editor/loadEditorScene.ts` handles editor-only folder upload parsing.
|
||||||
this.audio.destroy();
|
- `src/utils/loadMapSceneData.ts` is shared by the game scene and editor to load `public/map.json` and resolve model URLs.
|
||||||
this.zone.destroy();
|
- `src/types/editor.ts` contains the shared `MapNode`, `SceneData`, and `TransformMode` types.
|
||||||
GameManager._instance = null;
|
|
||||||
}
|
## Map Data
|
||||||
}
|
|
||||||
```
|
- `public/map.json` is expected to be a `MapNode[]`.
|
||||||
|
- Each map node `name` maps to `public/models/{name}/model.gltf`.
|
||||||
Usage:
|
- The editor renders a fallback cube for missing models.
|
||||||
|
- The game scene filters out nodes whose model cannot be resolved.
|
||||||
```ts
|
|
||||||
const game = GameManager.getInstance();
|
## Current Limitations
|
||||||
game.startMission("workshop");
|
|
||||||
```
|
- The repository is a prototype, not the full intended game runtime.
|
||||||
|
- `src/world/debug/TestScene.tsx` is part of the active scene composition.
|
||||||
**Important:** scene objects such as `Map`, `WorkshopZone`, `Lighting`, or `Environment` are **not** singletons and must remain standard React components.
|
- There is no central gameplay orchestrator such as `GameManager`.
|
||||||
|
- Missions, zones, cinematics, and dialogue systems are not implemented.
|
||||||
---
|
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
|
||||||
|
- Editor save-to-server is implemented as a Vite dev-server plugin, not a production backend API.
|
||||||
## 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");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# Editor Technical Notes
|
||||||
|
|
||||||
|
This document describes the map editor that exists in the current codebase.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The editor is a React route used to inspect and adjust the `public/map.json` scene data from inside the La-Fabrik app. It shares the same `MapNode` data format as the game scene and uses React Three Fiber for rendering.
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
- `/` renders the playable La-Fabrik scene.
|
||||||
|
- `/editor` renders the map editor.
|
||||||
|
- `src/App.tsx` mounts TanStack Router through `RouterProvider`.
|
||||||
|
- `src/router.tsx` defines the `/editor` route and imports `EditorPage` from `src/pages/editor/page.tsx`.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/
|
||||||
|
├── pages/
|
||||||
|
│ └── editor/
|
||||||
|
│ └── page.tsx
|
||||||
|
├── components/
|
||||||
|
│ └── editor/
|
||||||
|
│ ├── EditorControls.tsx
|
||||||
|
│ └── scene/
|
||||||
|
│ ├── EditorMap.tsx
|
||||||
|
│ └── EditorScene.tsx
|
||||||
|
├── controls/
|
||||||
|
│ └── editor/
|
||||||
|
│ └── FlyController.tsx
|
||||||
|
├── hooks/
|
||||||
|
│ └── editor/
|
||||||
|
│ ├── useEditorHistory.ts
|
||||||
|
│ └── useEditorSceneData.ts
|
||||||
|
├── types/
|
||||||
|
│ └── editor.ts
|
||||||
|
└── utils/
|
||||||
|
├── editor/
|
||||||
|
│ └── loadEditorScene.ts
|
||||||
|
└── loadMapSceneData.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
`src/pages/editor/page.tsx` is the route-level composition component. It owns route-specific state such as selected object, hovered object, transform mode, and player-mode toggle.
|
||||||
|
|
||||||
|
`src/hooks/editor/useEditorSceneData.ts` loads the default map data and handles folder uploads.
|
||||||
|
|
||||||
|
`src/hooks/editor/useEditorHistory.ts` owns editor undo and redo history.
|
||||||
|
|
||||||
|
`src/components/editor/scene/EditorScene.tsx` composes the editor canvas scene, camera controls, lights, keyboard shortcuts, and `EditorMap`.
|
||||||
|
|
||||||
|
`src/components/editor/scene/EditorMap.tsx` renders map nodes, fallback cubes, selection highlighting, and transform controls.
|
||||||
|
|
||||||
|
`src/components/editor/EditorControls.tsx` renders the HTML control panel outside the canvas.
|
||||||
|
|
||||||
|
`src/controls/editor/FlyController.tsx` provides editor movement controls for player-style navigation.
|
||||||
|
|
||||||
|
`src/utils/loadMapSceneData.ts` is shared by the game map and editor. It loads `/map.json` and resolves available `public/models/{name}/model.gltf` files.
|
||||||
|
|
||||||
|
`src/utils/editor/loadEditorScene.ts` contains editor-only upload handling for user-selected folders.
|
||||||
|
|
||||||
|
## Data Format
|
||||||
|
|
||||||
|
The shared editor type lives in `src/types/editor.ts`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface MapNode {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
position: [number, number, number];
|
||||||
|
rotation: [number, number, number];
|
||||||
|
scale: [number, number, number];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`public/map.json` is expected to be a `MapNode[]`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "pylone",
|
||||||
|
"type": "Mesh",
|
||||||
|
"position": [0, 5, 0],
|
||||||
|
"rotation": [0, 1.57, 0],
|
||||||
|
"scale": [1, 1, 1]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Each node `name` maps to a model folder:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
public/
|
||||||
|
├── map.json
|
||||||
|
└── models/
|
||||||
|
└── pylone/
|
||||||
|
└── model.gltf
|
||||||
|
```
|
||||||
|
|
||||||
|
If a model is missing, the editor renders a fallback cube so the node can still be selected and transformed.
|
||||||
|
|
||||||
|
## Editor Flow
|
||||||
|
|
||||||
|
1. `EditorPage` mounts on `/editor`.
|
||||||
|
2. `useEditorSceneData` calls `loadMapSceneData()`.
|
||||||
|
3. `loadMapSceneData()` loads `/map.json` and available model URLs.
|
||||||
|
4. If `/map.json` is missing, the page displays a folder-upload flow.
|
||||||
|
5. `EditorScene` renders the grid, lights, camera controls, and map nodes.
|
||||||
|
6. `EditorControls` exposes transform mode, history actions, export, save, and selection info.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- Click: select a node.
|
||||||
|
- `Esc`: clear selection.
|
||||||
|
- `T`: translate mode.
|
||||||
|
- `R`: rotate mode.
|
||||||
|
- `S`: scale mode.
|
||||||
|
- `Ctrl+Z` or `Cmd+Z`: undo.
|
||||||
|
- `Ctrl+Y` or `Cmd+Y`: redo.
|
||||||
|
- `WASD`, `ZQSD`, or arrow keys: move in player-controller mode.
|
||||||
|
- `Space`: move upward in player-controller mode.
|
||||||
|
- `Shift`: move downward in player-controller mode.
|
||||||
|
|
||||||
|
## Saving And Exporting
|
||||||
|
|
||||||
|
The editor supports two output paths:
|
||||||
|
|
||||||
|
- Export JSON downloads the current `MapNode[]` as `map.json`.
|
||||||
|
- Save to Server posts the current `MapNode[]` to `/api/save-map`.
|
||||||
|
|
||||||
|
The dev-only `/api/save-map` endpoint is implemented by the Vite plugin in `vite.config.ts`. It writes to `public/map.json` and enforces a maximum payload size.
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
Editor styles are in `src/index.css` under the `/* Editor page */` section. Classes are prefixed with `editor-` to avoid collisions with the game UI.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- Uploaded model object URLs are not currently revoked after replacement or unmount.
|
||||||
|
- Large `map.json` files are not virtualized, culled, or LOD-managed.
|
||||||
|
- There is no snap-to-grid, duplication, material editing, or object creation workflow.
|
||||||
|
- Save to Server is a Vite dev-server helper, not a production backend API.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Target Architecture
|
||||||
|
|
||||||
|
This document describes the intended medium-term architecture for the project.
|
||||||
|
|
||||||
|
## Relationship To The Current Code
|
||||||
|
|
||||||
|
- `docs/technical/architecture.md` is the source of truth for what exists now.
|
||||||
|
- This document describes intended direction, not implemented behavior.
|
||||||
|
- If this document conflicts with the current implementation, the current implementation wins.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Keep `App.tsx` small and orchestration-oriented.
|
||||||
|
- Separate production world code from debug-only runtime paths.
|
||||||
|
- Keep one clear source of truth per concern.
|
||||||
|
- Grow gameplay systems incrementally instead of prebuilding empty architecture.
|
||||||
|
|
||||||
|
## Intended Layers
|
||||||
|
|
||||||
|
### App Layer
|
||||||
|
|
||||||
|
- `App.tsx` mounts the canvas scene and top-level HTML overlays.
|
||||||
|
- It should stay thin and avoid gameplay logic.
|
||||||
|
|
||||||
|
### World Layer
|
||||||
|
|
||||||
|
- `src/world/` should contain production scene composition and production scene objects.
|
||||||
|
- Expected responsibilities:
|
||||||
|
- world composition
|
||||||
|
- map, environment, lighting
|
||||||
|
- player controller
|
||||||
|
- production interaction anchors
|
||||||
|
- production post-processing, if needed
|
||||||
|
|
||||||
|
### Debug Layer
|
||||||
|
|
||||||
|
- Debug-only scenes and tooling should be isolated from the production world path.
|
||||||
|
- Expected responsibilities:
|
||||||
|
- `lil-gui`
|
||||||
|
- performance overlay
|
||||||
|
- scene helpers
|
||||||
|
- free camera and calibration controls
|
||||||
|
- debug test scenes used during development
|
||||||
|
|
||||||
|
### UI Layer
|
||||||
|
|
||||||
|
- `src/components/ui/` should contain player-facing HTML overlays.
|
||||||
|
- Candidate examples:
|
||||||
|
- crosshair
|
||||||
|
- loading flow
|
||||||
|
- mission HUD
|
||||||
|
- narrative overlays
|
||||||
|
|
||||||
|
### Gameplay Layer
|
||||||
|
|
||||||
|
- As the project grows, gameplay state can move toward a clearer orchestration layer.
|
||||||
|
- Likely concerns:
|
||||||
|
- missions
|
||||||
|
- zones
|
||||||
|
- cinematics
|
||||||
|
- dialogue
|
||||||
|
- audio
|
||||||
|
- interactions
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Prefer direct, working code over speculative scaffolding.
|
||||||
|
- Shared types should stay close to their domain until they have multiple real consumers.
|
||||||
|
- Avoid creating new managers or service layers without an active runtime need.
|
||||||
|
- Debug-only runtime paths should be clearly marked and easy to remove when obsolete.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Editor User Guide
|
||||||
|
|
||||||
|
The map editor is available at `/editor`. It is a browser-based tool for inspecting and adjusting the objects listed in `public/map.json`.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Use the editor when you need to move, rotate, or scale existing map objects without editing JSON by hand.
|
||||||
|
|
||||||
|
The editor reads the same map data as the runtime scene:
|
||||||
|
|
||||||
|
- `public/map.json` contains the object list.
|
||||||
|
- `public/models/{name}/model.gltf` contains the matching 3D model for each object name.
|
||||||
|
- Missing models are displayed as gray fallback cubes, so incomplete maps remain editable.
|
||||||
|
|
||||||
|
## Map Node Format
|
||||||
|
|
||||||
|
Each entry in `public/map.json` represents one object:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| ---------- | ------------------------------------------------- |
|
||||||
|
| `name` | Model folder name in `public/models/{name}` |
|
||||||
|
| `type` | Object category |
|
||||||
|
| `position` | Object position as `[x, y, z]` |
|
||||||
|
| `rotation` | Object rotation as `[x, y, z]`, expressed radians |
|
||||||
|
| `scale` | Object scale as `[x, y, z]` |
|
||||||
|
|
||||||
|
## Editing Workflow
|
||||||
|
|
||||||
|
1. Open `/editor` in the local app.
|
||||||
|
2. Click an object in the scene to select it.
|
||||||
|
3. Choose a transform mode: translate, rotate, or scale.
|
||||||
|
4. Drag the transform gizmo in the 3D view.
|
||||||
|
5. Check the JSON inspector if you need exact values.
|
||||||
|
6. Use undo or redo if the transform is not correct.
|
||||||
|
7. Export the JSON or save it to the dev server.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
| Action | Input |
|
||||||
|
| -------------------- | -------------------------- |
|
||||||
|
| Select object | Click object |
|
||||||
|
| Deselect | `Esc` or click empty space |
|
||||||
|
| Translate mode | `T` |
|
||||||
|
| Rotate mode | `R` |
|
||||||
|
| Scale mode | `S` |
|
||||||
|
| Undo | `Ctrl+Z` |
|
||||||
|
| Redo | `Ctrl+Y` |
|
||||||
|
| Locked view movement | `WASD`, `ZQSD`, arrows |
|
||||||
|
| Move up | `Space` |
|
||||||
|
| Move down | `Shift` |
|
||||||
|
|
||||||
|
## View Mode
|
||||||
|
|
||||||
|
The `Lock view` action switches the editor into a movement mode closer to the runtime player camera. Use it to navigate larger scenes while keeping the transform tools available.
|
||||||
|
|
||||||
|
## JSON Inspector
|
||||||
|
|
||||||
|
The side panel includes a raw JSON inspector:
|
||||||
|
|
||||||
|
- When no object is selected, it shows the full map node list.
|
||||||
|
- When an object is selected, it highlights the JSON lines for that object.
|
||||||
|
|
||||||
|
This is useful for checking numeric transform values before saving or exporting.
|
||||||
|
|
||||||
|
## Saving Changes
|
||||||
|
|
||||||
|
### Export JSON
|
||||||
|
|
||||||
|
`Export JSON` downloads the current map node list as `map.json`. Use this when you want to manually replace `public/map.json`.
|
||||||
|
|
||||||
|
### Save To Server
|
||||||
|
|
||||||
|
`Save to server` is available only during local development. It writes the edited map back to `public/map.json` through the Vite dev-server endpoint.
|
||||||
|
|
||||||
|
The button is hidden in production builds because production persistence is not implemented.
|
||||||
|
|
||||||
|
## Current Limitations
|
||||||
|
|
||||||
|
- The editor only modifies existing nodes.
|
||||||
|
- It does not create or delete objects.
|
||||||
|
- It does not edit model files or textures.
|
||||||
|
- It does not provide production persistence.
|
||||||
|
- Fallback cubes indicate missing models; they are editor placeholders, not exported assets.
|
||||||
+63
-2
@@ -1,3 +1,64 @@
|
|||||||
# Features
|
# Implemented Features
|
||||||
|
|
||||||
TODO: Documenter les fonctionnalités du jeu.
|
This document lists features that are implemented in the current codebase.
|
||||||
|
|
||||||
|
## Scene
|
||||||
|
|
||||||
|
- Fullscreen React Three Fiber scene
|
||||||
|
- Main map scene loaded from `public/map.json` and matching `public/models/{name}/model.gltf` assets
|
||||||
|
- Debug physics test scene selectable from the debug panel
|
||||||
|
- Ambient and directional lighting
|
||||||
|
- Environment background setup
|
||||||
|
|
||||||
|
## Player
|
||||||
|
|
||||||
|
- Player camera mode
|
||||||
|
- Pointer lock mouse look
|
||||||
|
- Movement with `ZQSD`
|
||||||
|
- Jumping
|
||||||
|
- Octree-based collision against the loaded map
|
||||||
|
|
||||||
|
## Interactions
|
||||||
|
|
||||||
|
- Focus detection by distance and raycast
|
||||||
|
- Trigger interactions activated with `E`
|
||||||
|
- Grab interactions activated with the primary mouse button
|
||||||
|
- Interaction prompt shown for trigger interactions
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
- One-shot sound playback for trigger interactions
|
||||||
|
- Simple per-sound pooling through `AudioManager`
|
||||||
|
|
||||||
|
## Debug Tooling
|
||||||
|
|
||||||
|
- `?debug` query param enables the debug panel
|
||||||
|
- `lil-gui` controls for camera mode, scene mode, and interaction spheres
|
||||||
|
- Debug scene helpers
|
||||||
|
- Free debug camera
|
||||||
|
- `r3f-perf` overlay
|
||||||
|
|
||||||
|
## Map Editor
|
||||||
|
|
||||||
|
- `/editor` route for inspecting and editing `public/map.json`
|
||||||
|
- Automatic loading of `public/map.json` when available
|
||||||
|
- Folder upload fallback when `map.json` is missing
|
||||||
|
- Rendering of available `public/models/{name}/model.gltf` assets
|
||||||
|
- Fallback cubes for nodes whose model is missing
|
||||||
|
- Object selection by click
|
||||||
|
- Transform modes for translate, rotate, and scale
|
||||||
|
- Keyboard shortcuts for `T`, `R`, `S`, `Esc`, undo, and redo
|
||||||
|
- Player-style navigation mode with `WASD`, `ZQSD`, arrow keys, `Space`, and `Shift`
|
||||||
|
- JSON export for downloading the edited map
|
||||||
|
- Dev-server save endpoint for writing changes back to `public/map.json`
|
||||||
|
|
||||||
|
## Not Implemented Yet
|
||||||
|
|
||||||
|
- mission system
|
||||||
|
- zone system
|
||||||
|
- cinematic system
|
||||||
|
- dialogue system
|
||||||
|
- loading flow
|
||||||
|
- minimap and mission HUD
|
||||||
|
- full production separation between gameplay and debug scenes
|
||||||
|
- production backend persistence for editor saves
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[phases.setup]
|
||||||
|
nixPkgs = ["nodejs"]
|
||||||
Generated
+1811
-444
File diff suppressed because it is too large
Load Diff
+13
-1
@@ -3,6 +3,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
@@ -18,10 +21,15 @@
|
|||||||
"@react-three/fiber": "^9.6.0",
|
"@react-three/fiber": "^9.6.0",
|
||||||
"@react-three/postprocessing": "^3.0.4",
|
"@react-three/postprocessing": "^3.0.4",
|
||||||
"@react-three/rapier": "^2.2.0",
|
"@react-three/rapier": "^2.2.0",
|
||||||
|
"@tanstack/react-router": "^1.168.25",
|
||||||
"gsap": "^3.15.0",
|
"gsap": "^3.15.0",
|
||||||
|
"lil-gui": "^0.21.0",
|
||||||
|
"lucide-react": "^1.11.0",
|
||||||
"r3f-perf": "^7.2.3",
|
"r3f-perf": "^7.2.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"three": "^0.183.2"
|
"three": "^0.183.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -36,10 +44,14 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
"lil-gui": "^0.21.0",
|
|
||||||
"prettier": "^3.8.2",
|
"prettier": "^3.8.2",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.58.0",
|
"typescript-eslint": "^8.58.0",
|
||||||
"vite": "^8.0.4"
|
"vite": "^8.0.4"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"r3f-perf": {
|
||||||
|
"@react-three/drei": "$@react-three/drei"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4587
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
# public/models/environment/\*
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# public/models/farm/\*
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# public/models/general/\*
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:cc33b99b9334f9db2ec496f20b73b503e4aa50d88bec2e8c98579856d69cd7a0
|
|
||||||
size 11654940
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:311989a67f7ffd19bf30bb5ea2faabd4d2d58c33b6cb0cf9af1bf6ce1ec24bcf
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3dbf4a607d88f01b1ccb6702c5600876ae9339ed6ef017b44f04da18045cb3ec
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:43b2e1ee941e3a640c8d010073e777d84d49080fc2da0a94673b5bf27f3bf932
|
|
||||||
size 15766
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:b520c353a2bee68749ab5b3bcfc8790cab6c6d768b84f8140acd9b4137b46ddb
|
|
||||||
size 504386
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:6dd80b377721e70c5721f67e8241036179e27cd3d432d72f7c6022e97c586bd0
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:cac93257ca84d489a73a4a132d7c128f7d8f17130c2c0f366e3e758323294527
|
|
||||||
size 327465
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:9bc92d22231f432b9daef4887d29cffb76f5df323bd963618c64b215b711164f
|
|
||||||
size 15766
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:993c755409766ef7cb048ada6d22b0558c089542fb062fa74ac2e7e4238c3ec1
|
|
||||||
size 847081
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3dbf4a607d88f01b1ccb6702c5600876ae9339ed6ef017b44f04da18045cb3ec
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0e1d69c50347120c0c95c001482653c59c0a3c47deae20c552543d1b92d37c5d
|
|
||||||
size 12332
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:2f472061035616b0d31e3623fd89ea515deef730211701418d7dd230e8862d37
|
|
||||||
size 757042
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:32ca15471c5de27853efc168fe194efbae69b5e6ae9e8ea4afb24b43488ff6c9
|
|
||||||
size 298418
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:1073fc36eebaf28b13640845ff6de475fda4ebf86e05cf7a99eec7ed6dde4aba
|
|
||||||
size 439434
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:4ff25c8213d9810f20fc8fd19e89d4000621df2beefc2d42b4ef0097940b78b5
|
|
||||||
size 182316
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:fd679eb6797a1238b8cb6a0a67d667a1dd20f9fb48632a7d8657af8a7dafe0d2
|
|
||||||
size 1267604
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3dbf4a607d88f01b1ccb6702c5600876ae9339ed6ef017b44f04da18045cb3ec
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0e1d69c50347120c0c95c001482653c59c0a3c47deae20c552543d1b92d37c5d
|
|
||||||
size 12332
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:cdbd33ac4eb90e5b545812c2ac380a8584980bb2d0ee882ecbc2e758c93e6899
|
|
||||||
size 400516
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f9524a636fc86db283ac992df8e703cfb5a5e7eaded75cfabfd98b6560016818
|
|
||||||
size 254027
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f9524a636fc86db283ac992df8e703cfb5a5e7eaded75cfabfd98b6560016818
|
|
||||||
size 254027
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:d6b64adf48e75d5eb0c2afdf75898ba78158a78f29b33ef1c7b9b53803fe7c4b
|
|
||||||
size 29747
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:c6cf0a3ea97ec0a9645e11ddbd757d15d3bdfda540bade8f26bb1bed1773226e
|
|
||||||
size 15766
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3dbf4a607d88f01b1ccb6702c5600876ae9339ed6ef017b44f04da18045cb3ec
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0e1d69c50347120c0c95c001482653c59c0a3c47deae20c552543d1b92d37c5d
|
|
||||||
size 12332
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:543df1f679770de0c3f9185083526c03a726b8278cc9282fbcef0063caee6ed5
|
|
||||||
size 712996
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:6dd80b377721e70c5721f67e8241036179e27cd3d432d72f7c6022e97c586bd0
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:6dd80b377721e70c5721f67e8241036179e27cd3d432d72f7c6022e97c586bd0
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:9bc92d22231f432b9daef4887d29cffb76f5df323bd963618c64b215b711164f
|
|
||||||
size 15766
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:76723e5bc7d8fe5ab954f316613a27ff8d0796a19dfb2aef13d7530d74c3c6d9
|
|
||||||
size 233964
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3dbf4a607d88f01b1ccb6702c5600876ae9339ed6ef017b44f04da18045cb3ec
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0e1d69c50347120c0c95c001482653c59c0a3c47deae20c552543d1b92d37c5d
|
|
||||||
size 12332
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:a7c35f3309eceb828a87207b23c36fefad59a6c89d18f38c5f2eb386d47be06e
|
|
||||||
size 319800
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:6dd80b377721e70c5721f67e8241036179e27cd3d432d72f7c6022e97c586bd0
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:4d7eb1095d25a9c7e9aa58373478ba9c783216e4e32eb056d1f53c42002edea0
|
|
||||||
size 16802
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:9bc92d22231f432b9daef4887d29cffb76f5df323bd963618c64b215b711164f
|
|
||||||
size 15766
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:9c2c9b5498fab82f7d2ea60d25ea55892d826f5de1549a391f74ccd99fce67cc
|
|
||||||
size 195872
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:9b97d8b8d019d246924f6731aa03ed8f36169f75e1d5b98d09c09e7f96b08eb9
|
|
||||||
size 752957
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3dbf4a607d88f01b1ccb6702c5600876ae9339ed6ef017b44f04da18045cb3ec
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0e1d69c50347120c0c95c001482653c59c0a3c47deae20c552543d1b92d37c5d
|
|
||||||
size 12332
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:d7ac13a3a383243a991c7bace0de4c3a1a57e2d1f76fc5ae78419bdf4192715b
|
|
||||||
size 948507
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:efd017718813e29db22cf18d3caad987aa631b7c6500f1dbb03731d01d590787
|
|
||||||
size 117180
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f1e604806defcd692e1b8b1dde2df3d74dab111cad818caee6587f6ddff825ce
|
|
||||||
size 487294
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:81d4cbb54f7cf13737e53f8a2dbef07bea84f5d91305e67f289f0a57193fd2be
|
|
||||||
size 25110
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:51bd212f1786589fe327011e317d06a87ac1753c97f783a58d5dd829e61fb150
|
|
||||||
size 761672
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3dbf4a607d88f01b1ccb6702c5600876ae9339ed6ef017b44f04da18045cb3ec
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0e1d69c50347120c0c95c001482653c59c0a3c47deae20c552543d1b92d37c5d
|
|
||||||
size 12332
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:ec5e374fdf53803274f3db4f99a0ff73dd8ff98c9d39e00ada3df17e161de16d
|
|
||||||
size 517383
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:6ee5ae4da883ed21807192e3ab78a327b3b32f7ac4fd7625f7c0027191a2db8a
|
|
||||||
size 167195
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:eb9948e4d989471eb977e172fff0fa8e26545f64915847d7b29c80da4d31e480
|
|
||||||
size 167040
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f7f1b8d05ddf8991d200d55ee2a51e8ec194ed3c25d4e365d841197639521165
|
|
||||||
size 33325
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:84e69fbc325447188e4561b2709bc6b7f494b2eb942d3f292533c7d65eb1c301
|
|
||||||
size 350884
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3dbf4a607d88f01b1ccb6702c5600876ae9339ed6ef017b44f04da18045cb3ec
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0e1d69c50347120c0c95c001482653c59c0a3c47deae20c552543d1b92d37c5d
|
|
||||||
size 12332
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0d2f0cb413ff44ab6235c318ca9640afde4f67122efd75c01bd44a70135ded23
|
|
||||||
size 556212
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:dcb2a2e534e728bc6937d5c984cd4c63dd226e432c52d0973b5ffc53fab331fc
|
|
||||||
size 126366
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:2528c1e7b076b11bf6462215a6a797ee468c96648b6b213edc976f8e10483b26
|
|
||||||
size 167854
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:954e6b09f05c3848e26f5fc4a68f8419a387d38456698c2137682d1a854d950c
|
|
||||||
size 75784
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:14e25fcf300b7895a17fbe073fd4ef58e9ffda6cf6913ffa9b5c86f2d636f43e
|
|
||||||
size 59105
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3dbf4a607d88f01b1ccb6702c5600876ae9339ed6ef017b44f04da18045cb3ec
|
|
||||||
size 15765
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:2c6fc8e0d3d23d39b9293e6cf95b641c2458ea9d1b140e51f3cfd0d8b6ba1788
|
|
||||||
size 48818
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:37faa3d88ee97556f2935c13feb309f06551042f94e02878cca65c1bf0edc893
|
|
||||||
size 13356
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:5f1be2118699edd31eb3e4657a79c1c998d08f03f679e33545dd603ddee498d6
|
|
||||||
size 30651
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:7419843e9e57550f2e7dbf8762f350451b455c3ab9a379d1b281184212df5182
|
|
||||||
size 30663
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:d2648942081ffd0c83d4415fd34941963d8d5d05eb838113bd1a56306475a07b
|
|
||||||
size 24392
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:4d4609e73fec0d94e7efc65521239647bf37c511489c7656e1925c1875744810
|
|
||||||
size 577758
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user