Merge pull request #2 from La-Fabrik-Durable/feat/implem-map-scene-physique
Feat/implem map scene physique
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+36
-370
@@ -1,381 +1,47 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
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` mounts the `Canvas`, the 3D `World`, the debug perf overlay, and the HTML overlays.
|
||||||
Global systems such as gameplay flow, cinematics, audio, and debug tooling are implemented as **manager classes**.
|
- `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
|
||||||
|
- the player rig when the active camera mode is `player`
|
||||||
|
- `src/world/Map.tsx` loads the main map model and builds the collision octree.
|
||||||
|
- `src/world/debug/TestScene.tsx` provides a debug-oriented interaction and physics scene.
|
||||||
|
- `src/world/player/PlayerComponent.tsx` mounts the camera and controller.
|
||||||
|
- `src/world/player/PlayerController.tsx` owns pointer lock movement, jump handling, and interaction input.
|
||||||
|
|
||||||
Consistency matters, but the codebase does **not** force the same lifecycle pattern on scene components and global services.
|
## Interaction Model
|
||||||
|
|
||||||
---
|
- `src/stateManager/InteractionManager.ts` is the current interaction state source.
|
||||||
|
- `src/components/3d/InteractableObject.tsx` handles focus detection through distance and raycasting.
|
||||||
|
- `src/components/3d/TriggerObject.tsx` implements trigger-style interactions.
|
||||||
|
- `src/components/3d/GrabbableObject.tsx` implements hold-and-release interactions.
|
||||||
|
- `src/hooks/useInteraction.ts` exposes the interaction snapshot to React UI.
|
||||||
|
- `src/components/ui/InteractPrompt.tsx` shows the `E` prompt for trigger interactions.
|
||||||
|
|
||||||
## 1. Singleton Pattern for Global Managers Only
|
## Audio
|
||||||
|
|
||||||
Only cross-cutting services use the singleton pattern.
|
- `src/stateManager/AudioManager.ts` currently provides pooled one-shot sound playback.
|
||||||
|
- Trigger interactions may play audio directly through `AudioManager`.
|
||||||
|
|
||||||
Examples:
|
## Debug System
|
||||||
|
|
||||||
- `GameManager`
|
- Debug mode is enabled with `?debug`.
|
||||||
- `CinematicManager`
|
- `src/utils/debug/Debug.ts` owns the `lil-gui` instance and debug controls.
|
||||||
- `AudioManager`
|
- `src/hooks/debug/useCameraMode.ts` and `src/hooks/debug/useSceneMode.ts` subscribe to debug state.
|
||||||
- `ZoneManager`
|
- `src/utils/debug/DebugPerf.tsx` lazily mounts `r3f-perf` in debug mode.
|
||||||
- `Debug`
|
- `src/utils/debug/scene/DebugHelpers.tsx` mounts debug helpers.
|
||||||
- `EventEmitter`
|
- `src/utils/debug/scene/DebugCameraControls.tsx` mounts the free debug camera.
|
||||||
|
|
||||||
These services must exist once, be accessible from anywhere, and coordinate the experience globally.
|
## Current Limitations
|
||||||
|
|
||||||
```ts
|
- The repository is still a prototype, not the full intended game runtime.
|
||||||
// stateManager/GameManager.ts
|
- `src/world/debug/TestScene.tsx` is still part of the active scene composition.
|
||||||
export class GameManager {
|
- There is no central gameplay orchestrator such as `GameManager` yet.
|
||||||
private static _instance: GameManager | null = null;
|
- Missions, zones, cinematics, and dialogue systems are not implemented.
|
||||||
|
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
|
||||||
cinematic!: CinematicManager;
|
|
||||||
audio!: AudioManager;
|
|
||||||
zone!: ZoneManager;
|
|
||||||
|
|
||||||
static getInstance(): GameManager {
|
|
||||||
if (!GameManager._instance) {
|
|
||||||
GameManager._instance = new GameManager();
|
|
||||||
}
|
|
||||||
return GameManager._instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
this.cinematic = CinematicManager.getInstance();
|
|
||||||
this.audio = AudioManager.getInstance();
|
|
||||||
this.zone = ZoneManager.getInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(): void {
|
|
||||||
this.cinematic.destroy();
|
|
||||||
this.audio.destroy();
|
|
||||||
this.zone.destroy();
|
|
||||||
GameManager._instance = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const game = GameManager.getInstance();
|
|
||||||
game.startMission("workshop");
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important:** scene objects such as `Map`, `WorkshopZone`, `Lighting`, or `Environment` are **not** singletons and must remain standard React components.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Scene Objects Are React Components, Not Manager Classes
|
|
||||||
|
|
||||||
All 3D scene objects are implemented as **declarative React components**.
|
|
||||||
|
|
||||||
This includes:
|
|
||||||
|
|
||||||
- maps
|
|
||||||
- lights
|
|
||||||
- environments
|
|
||||||
- player controllers
|
|
||||||
- zones
|
|
||||||
- interactive props
|
|
||||||
- postprocessing layers
|
|
||||||
|
|
||||||
This keeps the code aligned with the R3F runtime instead of rebuilding a parallel imperative engine.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// world/zones/WorkshopZone.tsx
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import * as THREE from "three";
|
|
||||||
import { useFrame } from "@react-three/fiber";
|
|
||||||
import { useGLTF } from "@react-three/drei";
|
|
||||||
|
|
||||||
export function WorkshopZone() {
|
|
||||||
const root = useRef<THREE.Group>(null);
|
|
||||||
const gltf = useGLTF("/models/workshop/ebike.glb");
|
|
||||||
const mixer = useRef<THREE.AnimationMixer | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
mixer.current = new THREE.AnimationMixer(gltf.scene);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mixer.current?.stopAllAction();
|
|
||||||
mixer.current = null;
|
|
||||||
};
|
|
||||||
}, [gltf.scene]);
|
|
||||||
|
|
||||||
useFrame((_, delta) => {
|
|
||||||
mixer.current?.update(delta);
|
|
||||||
});
|
|
||||||
|
|
||||||
return <primitive ref={root} object={gltf.scene.clone()} />;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Per-frame values such as movement, interpolation, camera smoothing, and physics must stay in:
|
|
||||||
|
|
||||||
- `useRef`
|
|
||||||
- `useFrame`
|
|
||||||
- Rapier bodies
|
|
||||||
- other frame-based systems
|
|
||||||
|
|
||||||
They must **never** go through React state.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Single Source of Truth for Durable Gameplay State
|
|
||||||
|
|
||||||
The project uses a single authoritative `GameManager` for durable gameplay state.
|
|
||||||
|
|
||||||
React components subscribe to that state through thin hooks.
|
|
||||||
Other managers communicate through `GameManager`, which acts as the main gameplay orchestrator.
|
|
||||||
|
|
||||||
High-frequency values such as movement, camera interpolation, or physics never go through React state and stay in refs or frame-based systems.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// stateManager/GameManager.ts
|
|
||||||
type Phase = "loading" | "intro" | "exploring" | "cinematic" | "outro";
|
|
||||||
type ZoneId = "workshop" | "powerGrid" | "farm" | null;
|
|
||||||
|
|
||||||
type GameSnapshot = {
|
|
||||||
phase: Phase;
|
|
||||||
activeZone: ZoneId;
|
|
||||||
missionId: string | null;
|
|
||||||
missionStep: number;
|
|
||||||
inputLocked: boolean;
|
|
||||||
dialogueId: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class GameManager {
|
|
||||||
private static _instance: GameManager | null = null;
|
|
||||||
private listeners = new Set<() => void>();
|
|
||||||
|
|
||||||
private state: GameSnapshot = {
|
|
||||||
phase: "loading",
|
|
||||||
activeZone: null,
|
|
||||||
missionId: null,
|
|
||||||
missionStep: 0,
|
|
||||||
inputLocked: false,
|
|
||||||
dialogueId: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
static getInstance(): GameManager {
|
|
||||||
if (!GameManager._instance) {
|
|
||||||
GameManager._instance = new GameManager();
|
|
||||||
}
|
|
||||||
return GameManager._instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
getState(): GameSnapshot {
|
|
||||||
return this.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(listener: () => void): () => void {
|
|
||||||
this.listeners.add(listener);
|
|
||||||
return () => this.listeners.delete(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private emit(): void {
|
|
||||||
this.listeners.forEach((cb) => cb());
|
|
||||||
}
|
|
||||||
|
|
||||||
setPhase(phase: Phase): void {
|
|
||||||
this.state.phase = phase;
|
|
||||||
this.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
setActiveZone(zone: ZoneId): void {
|
|
||||||
this.state.activeZone = zone;
|
|
||||||
this.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
startMission(id: string): void {
|
|
||||||
this.state.missionId = id;
|
|
||||||
this.state.missionStep = 0;
|
|
||||||
this.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// hooks/useGameState.ts
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { GameManager } from "@/stateManager/GameManager";
|
|
||||||
|
|
||||||
export function useGameState() {
|
|
||||||
const game = GameManager.getInstance();
|
|
||||||
const [state, setState] = useState(game.getState());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return game.subscribe(() => {
|
|
||||||
setState({ ...game.getState() });
|
|
||||||
});
|
|
||||||
}, [game]);
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This keeps the architecture simple:
|
|
||||||
|
|
||||||
- **GameManager** owns durable gameplay state
|
|
||||||
- **other managers** handle side effects
|
|
||||||
- **React components** render that state
|
|
||||||
- **R3F frame systems** handle fast-changing values
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Side Effects Stay in Specialized Managers
|
|
||||||
|
|
||||||
Managers other than `GameManager` should not become secondary state stores.
|
|
||||||
|
|
||||||
Their role is to manage side effects and specialized runtime logic, such as:
|
|
||||||
|
|
||||||
- GSAP timelines
|
|
||||||
- audio playback
|
|
||||||
- zone entry detection
|
|
||||||
- interaction triggers
|
|
||||||
- camera lock/unlock
|
|
||||||
- temporary event coordination
|
|
||||||
|
|
||||||
They can read from `GameManager`, react to its state, or notify it of important transitions.
|
|
||||||
|
|
||||||
Example flow:
|
|
||||||
|
|
||||||
```
|
|
||||||
Component / Hook
|
|
||||||
↓
|
|
||||||
GameManager.getInstance()
|
|
||||||
├── startMission('workshop')
|
|
||||||
├── cinematic.play('intro_workshop')
|
|
||||||
├── audio.playAmbience('workshop')
|
|
||||||
└── zone.setActive('workshop')
|
|
||||||
```
|
|
||||||
|
|
||||||
This keeps the dependency graph understandable while avoiding duplicated durable state.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Memory Management — Dispose Only What You Own
|
|
||||||
|
|
||||||
GPU memory must be cleaned carefully.
|
|
||||||
|
|
||||||
However, the project does **not** blindly deep-dispose every object on unmount.
|
|
||||||
Only resources explicitly created and owned by the current component or manager should be disposed.
|
|
||||||
|
|
||||||
This includes things like:
|
|
||||||
|
|
||||||
- custom materials
|
|
||||||
- render targets
|
|
||||||
- postprocessing passes
|
|
||||||
- manually created geometries
|
|
||||||
- manually created textures
|
|
||||||
- temporary clones with owned resources
|
|
||||||
|
|
||||||
Shared or cached assets must **not** be blindly disposed.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// utils/Dispose.ts
|
|
||||||
import * as THREE from "three";
|
|
||||||
|
|
||||||
export class Dispose {
|
|
||||||
static material(material: THREE.Material): void {
|
|
||||||
for (const value of Object.values(material)) {
|
|
||||||
if (value instanceof THREE.Texture) {
|
|
||||||
value.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
material.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
static mesh(mesh: THREE.Mesh): void {
|
|
||||||
mesh.geometry?.dispose();
|
|
||||||
|
|
||||||
const materials = Array.isArray(mesh.material)
|
|
||||||
? mesh.material
|
|
||||||
: [mesh.material];
|
|
||||||
|
|
||||||
for (const material of materials) {
|
|
||||||
if (material) this.material(material);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static renderTarget(rt: THREE.WebGLRenderTarget): void {
|
|
||||||
rt.texture.dispose();
|
|
||||||
rt.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Example usage:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
useEffect(() => {
|
|
||||||
const material = new THREE.ShaderMaterial({
|
|
||||||
vertexShader,
|
|
||||||
fragmentShader,
|
|
||||||
});
|
|
||||||
|
|
||||||
meshRef.current.material = material;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
Dispose.material(material);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rule:** disposal is ownership-based, not automatic and not blind.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Debug Utility
|
|
||||||
|
|
||||||
The debug panel can be activated by appending `?debug` to the URL:
|
|
||||||
|
|
||||||
`http://localhost:5173?debug`
|
|
||||||
|
|
||||||
All debug logic is centralized in `Debug.ts`.
|
|
||||||
Do not scatter debug checks across the codebase.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// utils/Debug.ts
|
|
||||||
import GUI from "lil-gui";
|
|
||||||
|
|
||||||
export class Debug {
|
|
||||||
private static _instance: Debug | null = null;
|
|
||||||
|
|
||||||
readonly active: boolean;
|
|
||||||
gui: GUI | null = null;
|
|
||||||
|
|
||||||
static getInstance(): Debug {
|
|
||||||
if (!Debug._instance) Debug._instance = new Debug();
|
|
||||||
return Debug._instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
this.active = new URLSearchParams(window.location.search).has("debug");
|
|
||||||
if (this.active) {
|
|
||||||
this.gui = new GUI({ title: "La-Fabrik Debug" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(): void {
|
|
||||||
this.gui?.destroy();
|
|
||||||
Debug._instance = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const debug = Debug.getInstance();
|
|
||||||
|
|
||||||
if (debug.active) {
|
|
||||||
debug.gui!.add(params, "bloomIntensity", 0, 3).name("Bloom");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -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,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 is intentionally aspirational.
|
||||||
|
- 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
|
||||||
|
- temporary test scenes used during development
|
||||||
|
|
||||||
|
### UI Layer
|
||||||
|
|
||||||
|
- `src/components/ui/` should contain player-facing HTML overlays.
|
||||||
|
- Expected future examples:
|
||||||
|
- crosshair
|
||||||
|
- loading flow
|
||||||
|
- mission HUD
|
||||||
|
- narrative overlays
|
||||||
|
|
||||||
|
### Gameplay Layer
|
||||||
|
|
||||||
|
- As the project grows, gameplay state can move toward a clearer orchestration layer.
|
||||||
|
- Likely future 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 later.
|
||||||
+48
-2
@@ -1,3 +1,49 @@
|
|||||||
# 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/models/map/model.gltf`
|
||||||
|
- 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|||||||
Generated
+212
-206
@@ -13,6 +13,7 @@
|
|||||||
"@react-three/postprocessing": "^3.0.4",
|
"@react-three/postprocessing": "^3.0.4",
|
||||||
"@react-three/rapier": "^2.2.0",
|
"@react-three/rapier": "^2.2.0",
|
||||||
"gsap": "^3.15.0",
|
"gsap": "^3.15.0",
|
||||||
|
"lil-gui": "^0.21.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",
|
||||||
@@ -30,7 +31,6 @@
|
|||||||
"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",
|
||||||
@@ -293,9 +293,9 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.2",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -305,9 +305,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.9.2",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -484,29 +484,43 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||||
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@humanfs/types": "^0.15.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.0"
|
"node": ">=18.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@humanfs/node": {
|
"node_modules/@humanfs/node": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.8",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
|
||||||
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@humanfs/core": "^0.19.1",
|
"@humanfs/core": "^0.19.2",
|
||||||
|
"@humanfs/types": "^0.15.0",
|
||||||
"@humanwhocodes/retry": "^0.4.0"
|
"@humanwhocodes/retry": "^0.4.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.0"
|
"node": ">=18.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@humanfs/types": {
|
||||||
|
"version": "0.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
|
||||||
|
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/module-importer": {
|
"node_modules/@humanwhocodes/module-importer": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||||
@@ -604,9 +618,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||||
"integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
|
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -623,9 +637,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.124.0",
|
"version": "0.127.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
|
||||||
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
|
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -796,9 +810,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
|
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -813,9 +827,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
|
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -830,9 +844,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
|
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -847,9 +861,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
|
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -864,9 +878,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
|
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -881,9 +895,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
|
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -901,9 +915,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
|
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -921,9 +935,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
|
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -941,9 +955,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
|
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -961,9 +975,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
|
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -981,9 +995,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
|
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1001,9 +1015,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
|
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1018,9 +1032,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
|
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -1028,18 +1042,18 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "1.9.2",
|
"@emnapi/core": "1.10.0",
|
||||||
"@emnapi/runtime": "1.9.2",
|
"@emnapi/runtime": "1.10.0",
|
||||||
"@napi-rs/wasm-runtime": "^1.1.3"
|
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
|
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1054,9 +1068,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
|
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1174,18 +1188,17 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/three": {
|
"node_modules/@types/three": {
|
||||||
"version": "0.183.1",
|
"version": "0.184.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.0.tgz",
|
||||||
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
|
"integrity": "sha512-4mY2tZAu0y0B0567w7013BBXSpsP0+Z48NJvmNo4Y/Pf76yCyz6Jw4P3tUVs10WuYNXXZ+wmHyGWpCek3amJxA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
"@tweenjs/tween.js": "~23.1.3",
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
"@types/stats.js": "*",
|
"@types/stats.js": "*",
|
||||||
"@types/webxr": ">=0.5.17",
|
"@types/webxr": ">=0.5.17",
|
||||||
"@webgpu/types": "*",
|
|
||||||
"fflate": "~0.8.2",
|
"fflate": "~0.8.2",
|
||||||
"meshoptimizer": "~1.0.1"
|
"meshoptimizer": "~1.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/three/node_modules/@dimforge/rapier3d-compat": {
|
"node_modules/@types/three/node_modules/@dimforge/rapier3d-compat": {
|
||||||
@@ -1201,17 +1214,17 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.58.2",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz",
|
||||||
"integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==",
|
"integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.12.2",
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
"@typescript-eslint/scope-manager": "8.58.2",
|
"@typescript-eslint/scope-manager": "8.59.0",
|
||||||
"@typescript-eslint/type-utils": "8.58.2",
|
"@typescript-eslint/type-utils": "8.59.0",
|
||||||
"@typescript-eslint/utils": "8.58.2",
|
"@typescript-eslint/utils": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.2",
|
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^2.5.0"
|
"ts-api-utils": "^2.5.0"
|
||||||
@@ -1224,7 +1237,7 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.58.2",
|
"@typescript-eslint/parser": "^8.59.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
"typescript": ">=4.8.4 <6.1.0"
|
"typescript": ">=4.8.4 <6.1.0"
|
||||||
}
|
}
|
||||||
@@ -1240,16 +1253,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.58.2",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz",
|
||||||
"integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
|
"integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.58.2",
|
"@typescript-eslint/scope-manager": "8.59.0",
|
||||||
"@typescript-eslint/types": "8.58.2",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.58.2",
|
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.2",
|
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||||
"debug": "^4.4.3"
|
"debug": "^4.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1265,14 +1278,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.58.2",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz",
|
||||||
"integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==",
|
"integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.58.2",
|
"@typescript-eslint/tsconfig-utils": "^8.59.0",
|
||||||
"@typescript-eslint/types": "^8.58.2",
|
"@typescript-eslint/types": "^8.59.0",
|
||||||
"debug": "^4.4.3"
|
"debug": "^4.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1287,14 +1300,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.58.2",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz",
|
||||||
"integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==",
|
"integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.58.2",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.2"
|
"@typescript-eslint/visitor-keys": "8.59.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -1305,9 +1318,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.58.2",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz",
|
||||||
"integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==",
|
"integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1322,15 +1335,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.58.2",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz",
|
||||||
"integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==",
|
"integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.58.2",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.58.2",
|
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||||
"@typescript-eslint/utils": "8.58.2",
|
"@typescript-eslint/utils": "8.59.0",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"ts-api-utils": "^2.5.0"
|
"ts-api-utils": "^2.5.0"
|
||||||
},
|
},
|
||||||
@@ -1347,9 +1360,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.58.2",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz",
|
||||||
"integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==",
|
"integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1361,16 +1374,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.58.2",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz",
|
||||||
"integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==",
|
"integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.58.2",
|
"@typescript-eslint/project-service": "8.59.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.58.2",
|
"@typescript-eslint/tsconfig-utils": "8.59.0",
|
||||||
"@typescript-eslint/types": "8.58.2",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.2",
|
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"minimatch": "^10.2.2",
|
"minimatch": "^10.2.2",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.3",
|
||||||
@@ -1441,16 +1454,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.58.2",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz",
|
||||||
"integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==",
|
"integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.9.1",
|
"@eslint-community/eslint-utils": "^4.9.1",
|
||||||
"@typescript-eslint/scope-manager": "8.58.2",
|
"@typescript-eslint/scope-manager": "8.59.0",
|
||||||
"@typescript-eslint/types": "8.58.2",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.58.2"
|
"@typescript-eslint/typescript-estree": "8.59.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -1465,13 +1478,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.58.2",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz",
|
||||||
"integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==",
|
"integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.58.2",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"eslint-visitor-keys": "^5.0.0"
|
"eslint-visitor-keys": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1556,12 +1569,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@webgpu/types": {
|
|
||||||
"version": "0.1.69",
|
|
||||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
|
|
||||||
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.16.0",
|
"version": "8.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||||
@@ -1586,9 +1593,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.14.0",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1653,9 +1660,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.18",
|
"version": "2.10.23",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz",
|
||||||
"integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==",
|
"integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1767,9 +1774,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001788",
|
"version": "1.0.30001791",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
|
||||||
"integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
|
"integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1927,9 +1934,9 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.336",
|
"version": "1.5.344",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
|
||||||
"integrity": "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==",
|
"integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -2064,9 +2071,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react-hooks": {
|
"node_modules/eslint-plugin-react-hooks": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
|
||||||
"integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
|
"integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2080,7 +2087,7 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
|
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react-refresh": {
|
"node_modules/eslint-plugin-react-refresh": {
|
||||||
@@ -2878,7 +2885,6 @@
|
|||||||
"version": "0.21.0",
|
"version": "0.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.21.0.tgz",
|
||||||
"integrity": "sha512-tpvxN7v1GvE/Tv+GRopfOp0W7fVEjF4PltkuX8vOCIfim22rD1ztvfkoEMcv9lzQeuNUSeIrUmUjBwmlW/oUew==",
|
"integrity": "sha512-tpvxN7v1GvE/Tv+GRopfOp0W7fVEjF4PltkuX8vOCIfim22rD1ztvfkoEMcv9lzQeuNUSeIrUmUjBwmlW/oUew==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
@@ -2946,9 +2952,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/meshoptimizer": {
|
"node_modules/meshoptimizer": {
|
||||||
"version": "1.0.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
|
||||||
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
|
"integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
@@ -3008,9 +3014,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.37",
|
"version": "2.0.38",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
|
||||||
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
|
"integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -3126,9 +3132,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.9",
|
"version": "8.5.12",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
|
||||||
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
|
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3155,12 +3161,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postprocessing": {
|
"node_modules/postprocessing": {
|
||||||
"version": "6.39.0",
|
"version": "6.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.0.tgz",
|
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.1.tgz",
|
||||||
"integrity": "sha512-/G6JY8hs426lcto/pBZlnFSkyEo1fHsh4gy7FPJtq1SaSUOzJgDW6f6f1K/+aMOYzK/eQEefyOb3++jPPIUeDA==",
|
"integrity": "sha512-R2dG2zy+BAx3USl5EHw+PvnrlbT5PKnZVp3se0HCR0pWH8WQdh742yNG4YWOsq6c0bFpffk0Gd2RqPeoP/wKng==",
|
||||||
"license": "Zlib",
|
"license": "Zlib",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"three": ">= 0.168.0 < 0.184.0"
|
"three": ">= 0.168.0 < 0.185.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/potpack": {
|
"node_modules/potpack": {
|
||||||
@@ -3180,9 +3186,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.8.2",
|
"version": "3.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
||||||
"integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
|
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -3521,14 +3527,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
|
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.124.0",
|
"@oxc-project/types": "=0.127.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-rc.15"
|
"@rolldown/pluginutils": "1.0.0-rc.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -3537,27 +3543,27 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
|
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
|
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
|
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
|
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
|
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
|
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
|
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
|
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
|
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
|
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -3842,9 +3848,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -3856,16 +3862,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.58.2",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz",
|
||||||
"integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==",
|
"integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||||
"@typescript-eslint/parser": "8.58.2",
|
"@typescript-eslint/parser": "8.59.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.58.2",
|
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||||
"@typescript-eslint/utils": "8.58.2"
|
"@typescript-eslint/utils": "8.59.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -3946,17 +3952,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.8",
|
"version": "8.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
||||||
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.10",
|
||||||
"rolldown": "1.0.0-rc.15",
|
"rolldown": "1.0.0-rc.17",
|
||||||
"tinyglobby": "^0.2.15"
|
"tinyglobby": "^0.2.16"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
|||||||
+1
-1
@@ -19,6 +19,7 @@
|
|||||||
"@react-three/postprocessing": "^3.0.4",
|
"@react-three/postprocessing": "^3.0.4",
|
||||||
"@react-three/rapier": "^2.2.0",
|
"@react-three/rapier": "^2.2.0",
|
||||||
"gsap": "^3.15.0",
|
"gsap": "^3.15.0",
|
||||||
|
"lil-gui": "^0.21.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",
|
||||||
@@ -36,7 +37,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -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:c98eb2de597f5e0fd60e2caff3e1889c761bfdcb9d23488d31700cb850cdd2ea
|
|
||||||
size 2800791
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b3535a67501bb43ccf233a25e98b20b3804e29f1fe7ef8ba821bbdd00b98f140
|
||||||
|
size 3279070
|
||||||
@@ -1 +0,0 @@
|
|||||||
# public/models/powergrid/\*
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# public/models/workshop/\*
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:904b303c98f865526b9524b955f440e630f29d8e18a57bb7bf443fcd9715add1
|
||||||
|
size 83079911
|
||||||
@@ -1 +0,0 @@
|
|||||||
# public/sounds/\*
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5f714331f0f9ad760ae59d1c7cd4a6eb10b853c018f9764564e306b2f2444e56
|
||||||
|
size 149972
|
||||||
@@ -1 +0,0 @@
|
|||||||
# public/textures/\*
|
|
||||||
-184
@@ -1,184 +0,0 @@
|
|||||||
.counter {
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: var(--accent);
|
|
||||||
background: var(--accent-bg);
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--accent-border);
|
|
||||||
}
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid var(--accent);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.base,
|
|
||||||
.framework,
|
|
||||||
.vite {
|
|
||||||
inset-inline: 0;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.base {
|
|
||||||
width: 170px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.framework,
|
|
||||||
.vite {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.framework {
|
|
||||||
z-index: 1;
|
|
||||||
top: 34px;
|
|
||||||
height: 28px;
|
|
||||||
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
|
||||||
scale(1.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vite {
|
|
||||||
z-index: 0;
|
|
||||||
top: 107px;
|
|
||||||
height: 26px;
|
|
||||||
width: auto;
|
|
||||||
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
|
||||||
scale(0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#center {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 25px;
|
|
||||||
place-content: center;
|
|
||||||
place-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
padding: 32px 20px 24px;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#next-steps {
|
|
||||||
display: flex;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
flex: 1 1 0;
|
|
||||||
padding: 32px;
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
padding: 24px 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#docs {
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#next-steps ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 32px 0 0;
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--text-h);
|
|
||||||
font-size: 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--social-bg);
|
|
||||||
display: flex;
|
|
||||||
padding: 6px 12px;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: box-shadow 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
.button-icon {
|
|
||||||
height: 18px;
|
|
||||||
width: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
margin-top: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
li {
|
|
||||||
flex: 1 1 calc(50% - 8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#spacer {
|
|
||||||
height: 88px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticks {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&::before,
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: -4.5px;
|
|
||||||
border: 5px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
left: 0;
|
|
||||||
border-left-color: var(--border);
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
right: 0;
|
|
||||||
border-right-color: var(--border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+15
-113
@@ -1,119 +1,21 @@
|
|||||||
import { useState } from "react";
|
import { Suspense } from "react";
|
||||||
import reactLogo from "./assets/react.svg";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import viteLogo from "./assets/vite.svg";
|
import { Crosshair } from "@/components/ui/Crosshair";
|
||||||
import heroImg from "./assets/hero.png";
|
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
||||||
import "./App.css";
|
import { DebugPerf } from "@/utils/debug/DebugPerf";
|
||||||
|
import { World } from "@/world/World";
|
||||||
function App() {
|
|
||||||
const [count, setCount] = useState(0);
|
|
||||||
|
|
||||||
|
function App(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section id="center">
|
<Canvas camera={{ position: [85, 60, 85], fov: 42 }} shadows>
|
||||||
<div className="hero">
|
<Suspense fallback={null}>
|
||||||
<img src={heroImg} className="base" width="170" height="179" alt="" />
|
<World />
|
||||||
<img src={reactLogo} className="framework" alt="React logo" />
|
<DebugPerf />
|
||||||
<img src={viteLogo} className="vite" alt="Vite logo" />
|
</Suspense>
|
||||||
</div>
|
</Canvas>
|
||||||
<div>
|
<Crosshair />
|
||||||
<h1>Get started</h1>
|
<InteractPrompt />
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="counter"
|
|
||||||
onClick={() => setCount((count) => count + 1)}
|
|
||||||
>
|
|
||||||
Count is {count}
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="ticks"></div>
|
|
||||||
|
|
||||||
<section id="next-steps">
|
|
||||||
<div id="docs">
|
|
||||||
<svg className="icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#documentation-icon"></use>
|
|
||||||
</svg>
|
|
||||||
<h2>Documentation</h2>
|
|
||||||
<p>Your questions, answered</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://vite.dev/" target="_blank">
|
|
||||||
<img className="logo" src={viteLogo} alt="" />
|
|
||||||
Explore Vite
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://react.dev/" target="_blank">
|
|
||||||
<img className="button-icon" src={reactLogo} alt="" />
|
|
||||||
Learn more
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div id="social">
|
|
||||||
<svg className="icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#social-icon"></use>
|
|
||||||
</svg>
|
|
||||||
<h2>Connect with us</h2>
|
|
||||||
<p>Join the Vite community</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://github.com/vitejs/vite" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#github-icon"></use>
|
|
||||||
</svg>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://chat.vite.dev/" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#discord-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://x.com/vite_js" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#x-icon"></use>
|
|
||||||
</svg>
|
|
||||||
X.com
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://bsky.app/profile/vite.dev" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#bluesky-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Bluesky
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="ticks"></div>
|
|
||||||
<section id="spacer"></section>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,140 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { RigidBody } from "@react-three/rapier";
|
||||||
|
import type { RapierRigidBody } from "@react-three/rapier";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { InteractableObject } from "@/components/3d/InteractableObject";
|
||||||
|
import {
|
||||||
|
GRAB_DEFAULT_COLLIDERS,
|
||||||
|
GRAB_DEFAULT_LABEL,
|
||||||
|
GRAB_HOLD_DISTANCE_DEFAULT,
|
||||||
|
GRAB_HOLD_DISTANCE_MAX,
|
||||||
|
GRAB_HOLD_DISTANCE_MIN,
|
||||||
|
GRAB_HOLD_DISTANCE_STEP,
|
||||||
|
GRAB_STIFFNESS_DEFAULT,
|
||||||
|
GRAB_STIFFNESS_MAX,
|
||||||
|
GRAB_STIFFNESS_MIN,
|
||||||
|
GRAB_STIFFNESS_STEP,
|
||||||
|
GRAB_THROW_BOOST_DEFAULT,
|
||||||
|
GRAB_THROW_BOOST_MAX,
|
||||||
|
GRAB_THROW_BOOST_MIN,
|
||||||
|
GRAB_THROW_BOOST_STEP,
|
||||||
|
} from "@/data/grabConfig";
|
||||||
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
|
import type { ColliderShape, Vector3Tuple } from "@/types/3d";
|
||||||
|
|
||||||
|
interface GrabbableObjectProps {
|
||||||
|
position: Vector3Tuple;
|
||||||
|
children: React.ReactNode;
|
||||||
|
colliders?: ColliderShape;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared params let one debug folder drive every instance.
|
||||||
|
const params = {
|
||||||
|
stiffness: GRAB_STIFFNESS_DEFAULT,
|
||||||
|
throwBoost: GRAB_THROW_BOOST_DEFAULT,
|
||||||
|
holdDistance: GRAB_HOLD_DISTANCE_DEFAULT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZERO_ANGULAR_VELOCITY = { x: 0, y: 0, z: 0 };
|
||||||
|
|
||||||
|
const _holdTarget = new THREE.Vector3();
|
||||||
|
const _currentPos = new THREE.Vector3();
|
||||||
|
const _velocity = new THREE.Vector3();
|
||||||
|
|
||||||
|
export function GrabbableObject({
|
||||||
|
position,
|
||||||
|
children,
|
||||||
|
colliders = GRAB_DEFAULT_COLLIDERS,
|
||||||
|
label = GRAB_DEFAULT_LABEL,
|
||||||
|
}: GrabbableObjectProps): React.JSX.Element {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
const rbRef = useRef<RapierRigidBody>(null);
|
||||||
|
const isHolding = useRef(false);
|
||||||
|
|
||||||
|
useDebugFolder("GrabbableObject", (folder) => {
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
params,
|
||||||
|
"stiffness",
|
||||||
|
GRAB_STIFFNESS_MIN,
|
||||||
|
GRAB_STIFFNESS_MAX,
|
||||||
|
GRAB_STIFFNESS_STEP,
|
||||||
|
)
|
||||||
|
.name("Hold stiffness");
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
params,
|
||||||
|
"throwBoost",
|
||||||
|
GRAB_THROW_BOOST_MIN,
|
||||||
|
GRAB_THROW_BOOST_MAX,
|
||||||
|
GRAB_THROW_BOOST_STEP,
|
||||||
|
)
|
||||||
|
.name("Throw boost");
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
params,
|
||||||
|
"holdDistance",
|
||||||
|
GRAB_HOLD_DISTANCE_MIN,
|
||||||
|
GRAB_HOLD_DISTANCE_MAX,
|
||||||
|
GRAB_HOLD_DISTANCE_STEP,
|
||||||
|
)
|
||||||
|
.name("Hold distance");
|
||||||
|
});
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (!isHolding.current || !rbRef.current) return;
|
||||||
|
|
||||||
|
camera.getWorldDirection(_holdTarget);
|
||||||
|
_holdTarget.multiplyScalar(params.holdDistance).add(camera.position);
|
||||||
|
|
||||||
|
const t = rbRef.current.translation();
|
||||||
|
_currentPos.set(t.x, t.y, t.z);
|
||||||
|
|
||||||
|
_velocity
|
||||||
|
.subVectors(_holdTarget, _currentPos)
|
||||||
|
.multiplyScalar(params.stiffness);
|
||||||
|
|
||||||
|
rbRef.current.setLinvel(
|
||||||
|
{ x: _velocity.x, y: _velocity.y, z: _velocity.z },
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
rbRef.current.setAngvel(ZERO_ANGULAR_VELOCITY, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RigidBody
|
||||||
|
ref={rbRef}
|
||||||
|
type="dynamic"
|
||||||
|
colliders={colliders}
|
||||||
|
position={position}
|
||||||
|
>
|
||||||
|
<InteractableObject
|
||||||
|
kind="grab"
|
||||||
|
label={label}
|
||||||
|
position={position}
|
||||||
|
bodyRef={rbRef}
|
||||||
|
onPress={() => {
|
||||||
|
isHolding.current = true;
|
||||||
|
}}
|
||||||
|
onRelease={() => {
|
||||||
|
isHolding.current = false;
|
||||||
|
if (!rbRef.current || params.throwBoost === GRAB_THROW_BOOST_DEFAULT)
|
||||||
|
return;
|
||||||
|
const v = rbRef.current.linvel();
|
||||||
|
rbRef.current.setLinvel(
|
||||||
|
{
|
||||||
|
x: v.x * params.throwBoost,
|
||||||
|
y: v.y * params.throwBoost,
|
||||||
|
z: v.z * params.throwBoost,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</InteractableObject>
|
||||||
|
</RigidBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import type { RapierRigidBody } from "@react-three/rapier";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import type GUI from "lil-gui";
|
||||||
|
import type { RefObject } from "react";
|
||||||
|
import {
|
||||||
|
INTERACTION_DEBUG_SPHERE_COLOR,
|
||||||
|
INTERACTION_DEBUG_SPHERE_OPACITY,
|
||||||
|
INTERACTION_DEBUG_SPHERE_SEGMENTS,
|
||||||
|
} from "@/data/debugConfig";
|
||||||
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
|
import { InteractionManager } from "@/stateManager/InteractionManager";
|
||||||
|
import { INTERACTION_RADIUS } from "@/data/interactionConfig";
|
||||||
|
import type { Vector3Tuple } from "@/types/3d";
|
||||||
|
import type { InteractableHandle, InteractableKind } from "@/types/interaction";
|
||||||
|
|
||||||
|
interface InteractableObjectBaseProps {
|
||||||
|
label: string;
|
||||||
|
position: Vector3Tuple;
|
||||||
|
bodyRef?: RefObject<RapierRigidBody | null>;
|
||||||
|
onPress: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TriggerInteractableObjectProps extends InteractableObjectBaseProps {
|
||||||
|
kind: "trigger";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GrabInteractableObjectProps extends InteractableObjectBaseProps {
|
||||||
|
kind: "grab";
|
||||||
|
onRelease: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type InteractableObjectProps =
|
||||||
|
| TriggerInteractableObjectProps
|
||||||
|
| GrabInteractableObjectProps;
|
||||||
|
|
||||||
|
type MutableInteractableHandle = {
|
||||||
|
kind: InteractableKind;
|
||||||
|
label: string;
|
||||||
|
onPress: () => void;
|
||||||
|
onRelease?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _cameraPos = new THREE.Vector3();
|
||||||
|
const _cameraDir = new THREE.Vector3();
|
||||||
|
const _objectPos = new THREE.Vector3();
|
||||||
|
const _raycaster = new THREE.Raycaster();
|
||||||
|
|
||||||
|
export function InteractableObject(
|
||||||
|
props: InteractableObjectProps,
|
||||||
|
): React.JSX.Element {
|
||||||
|
const { kind, label, position, bodyRef, onPress, children } = props;
|
||||||
|
const onRelease = props.kind === "grab" ? props.onRelease : undefined;
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
const debugSphereRef = useRef<THREE.Mesh>(null);
|
||||||
|
|
||||||
|
const handle = useRef<InteractableHandle>(
|
||||||
|
props.kind === "grab"
|
||||||
|
? { kind: props.kind, label, onPress, onRelease: props.onRelease }
|
||||||
|
: { kind: props.kind, label, onPress },
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const current = handle.current as MutableInteractableHandle;
|
||||||
|
current.kind = kind;
|
||||||
|
current.label = label;
|
||||||
|
current.onPress = onPress;
|
||||||
|
|
||||||
|
if (kind === "grab" && onRelease) {
|
||||||
|
current.onRelease = onRelease;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete current.onRelease;
|
||||||
|
return undefined;
|
||||||
|
}, [kind, label, onPress, onRelease]);
|
||||||
|
|
||||||
|
const setupInteractionDebugFolder = useCallback((folder: GUI) => {
|
||||||
|
folder
|
||||||
|
.add({ radius: INTERACTION_RADIUS }, "radius")
|
||||||
|
.name("Interaction radius")
|
||||||
|
.disable();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useDebugFolder("Interaction", setupInteractionDebugFolder);
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
const group = groupRef.current;
|
||||||
|
const debug = Debug.getInstance();
|
||||||
|
const manager = InteractionManager.getInstance();
|
||||||
|
|
||||||
|
if (debugSphereRef.current) {
|
||||||
|
debugSphereRef.current.visible =
|
||||||
|
debug.active && debug.getShowInteractionSpheres();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyRef?.current) {
|
||||||
|
const t = bodyRef.current.translation();
|
||||||
|
_objectPos.set(t.x, t.y, t.z);
|
||||||
|
} else {
|
||||||
|
_objectPos.set(...position);
|
||||||
|
}
|
||||||
|
|
||||||
|
camera.getWorldPosition(_cameraPos);
|
||||||
|
const dist = _cameraPos.distanceTo(_objectPos);
|
||||||
|
|
||||||
|
if (dist > INTERACTION_RADIUS) {
|
||||||
|
if (manager.getState().focused === handle.current) {
|
||||||
|
manager.setFocused(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
camera.getWorldDirection(_cameraDir);
|
||||||
|
_raycaster.set(_cameraPos, _cameraDir);
|
||||||
|
_raycaster.far = INTERACTION_RADIUS;
|
||||||
|
|
||||||
|
const hits = group ? _raycaster.intersectObject(group, true) : [];
|
||||||
|
const validHit = hits.find((h) => h.object !== debugSphereRef.current);
|
||||||
|
|
||||||
|
if (validHit) {
|
||||||
|
manager.setFocused(handle.current);
|
||||||
|
} else if (manager.getState().focused === handle.current) {
|
||||||
|
manager.setFocused(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group ref={groupRef}>
|
||||||
|
{children}
|
||||||
|
<mesh ref={debugSphereRef} visible={false}>
|
||||||
|
<sphereGeometry
|
||||||
|
args={[
|
||||||
|
INTERACTION_RADIUS,
|
||||||
|
INTERACTION_DEBUG_SPHERE_SEGMENTS,
|
||||||
|
INTERACTION_DEBUG_SPHERE_SEGMENTS,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<meshBasicMaterial
|
||||||
|
color={INTERACTION_DEBUG_SPHERE_COLOR}
|
||||||
|
wireframe
|
||||||
|
transparent
|
||||||
|
opacity={INTERACTION_DEBUG_SPHERE_OPACITY}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/components/3d/InteractiveObject.tsx
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import { RigidBody } from "@react-three/rapier";
|
||||||
|
import { InteractableObject } from "@/components/3d/InteractableObject";
|
||||||
|
import {
|
||||||
|
TRIGGER_DEFAULT_COLLIDERS,
|
||||||
|
TRIGGER_DEFAULT_LABEL,
|
||||||
|
TRIGGER_DEFAULT_SOUND_VOLUME,
|
||||||
|
TRIGGER_DEFAULT_SPAWN_OFFSET,
|
||||||
|
} from "@/data/triggerConfig";
|
||||||
|
import { AudioManager } from "@/stateManager/AudioManager";
|
||||||
|
import type { ColliderShape, Vector3Tuple } from "@/types/3d";
|
||||||
|
|
||||||
|
interface SpawnedModel {
|
||||||
|
id: number;
|
||||||
|
position: Vector3Tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TriggerObjectProps {
|
||||||
|
position: Vector3Tuple;
|
||||||
|
children: React.ReactNode;
|
||||||
|
colliders?: ColliderShape;
|
||||||
|
label?: string;
|
||||||
|
soundPath?: string;
|
||||||
|
soundVolume?: number;
|
||||||
|
spawnModel?: string;
|
||||||
|
spawnOffset?: Vector3Tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _spawnCounter = 0;
|
||||||
|
|
||||||
|
function SpawnedModelInstance({
|
||||||
|
path,
|
||||||
|
position,
|
||||||
|
}: {
|
||||||
|
path: string;
|
||||||
|
position: Vector3Tuple;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const { scene } = useGLTF(path);
|
||||||
|
return <primitive object={scene.clone()} position={position} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TriggerObject({
|
||||||
|
position,
|
||||||
|
children,
|
||||||
|
colliders = TRIGGER_DEFAULT_COLLIDERS,
|
||||||
|
label = TRIGGER_DEFAULT_LABEL,
|
||||||
|
soundPath,
|
||||||
|
soundVolume = TRIGGER_DEFAULT_SOUND_VOLUME,
|
||||||
|
spawnModel,
|
||||||
|
spawnOffset = TRIGGER_DEFAULT_SPAWN_OFFSET,
|
||||||
|
}: TriggerObjectProps): React.JSX.Element {
|
||||||
|
const [spawned, setSpawned] = useState<SpawnedModel[]>([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RigidBody type="fixed" colliders={colliders} position={position}>
|
||||||
|
<InteractableObject
|
||||||
|
kind="trigger"
|
||||||
|
label={label}
|
||||||
|
position={position}
|
||||||
|
onPress={() => {
|
||||||
|
if (soundPath) {
|
||||||
|
AudioManager.getInstance().playSound(soundPath, soundVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spawnModel) {
|
||||||
|
const spawnPos: Vector3Tuple = [
|
||||||
|
position[0] + spawnOffset[0],
|
||||||
|
position[1] + spawnOffset[1],
|
||||||
|
position[2] + spawnOffset[2],
|
||||||
|
];
|
||||||
|
setSpawned((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ id: ++_spawnCounter, position: spawnPos },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</InteractableObject>
|
||||||
|
</RigidBody>
|
||||||
|
|
||||||
|
{spawnModel &&
|
||||||
|
spawned.map((s) => (
|
||||||
|
<SpawnedModelInstance
|
||||||
|
key={s.id}
|
||||||
|
path={spawnModel}
|
||||||
|
position={s.position}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/components/ui/CinematicBars.tsx
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
|
import { useInteraction } from "@/hooks/useInteraction";
|
||||||
|
|
||||||
|
export function Crosshair(): React.JSX.Element | null {
|
||||||
|
const cameraMode = useCameraMode();
|
||||||
|
const { focused } = useInteraction();
|
||||||
|
|
||||||
|
if (cameraMode !== "player") return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={focused ? "crosshair crosshair--interact" : "crosshair"}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { INTERACT_KEY } from "@/data/keybindings";
|
||||||
|
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
|
import { useInteraction } from "@/hooks/useInteraction";
|
||||||
|
|
||||||
|
export function InteractPrompt(): React.JSX.Element | null {
|
||||||
|
const cameraMode = useCameraMode();
|
||||||
|
const { focused, holding } = useInteraction();
|
||||||
|
|
||||||
|
if (cameraMode !== "player") return null;
|
||||||
|
if (!focused || holding || focused.kind !== "trigger") return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="interact-prompt" aria-live="polite">
|
||||||
|
<kbd className="interact-prompt__key">{INTERACT_KEY.toUpperCase()}</kbd>
|
||||||
|
<span className="interact-prompt__label">{focused.label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/components/ui/LoadingScreen.tsx
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/components/ui/MapHUD.tsx
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/components/ui/MissionHUD.tsx
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/components/ui/NarrativeOverlay.tsx
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export const INTERACTION_DEBUG_SPHERE_SEGMENTS = 16;
|
||||||
|
export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15";
|
||||||
|
export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25;
|
||||||
|
|
||||||
|
export const MAP_DEBUG_BOX_HELPER_COLOR = 0x00ff88;
|
||||||
|
|
||||||
|
export const DEBUG_CAMERA_DAMPING_FACTOR = 0.05;
|
||||||
|
export const DEBUG_CAMERA_MIN_DISTANCE = 100;
|
||||||
|
export const DEBUG_CAMERA_MAX_DISTANCE = 1000;
|
||||||
|
|
||||||
|
export const DEBUG_GRID_SIZE = 180;
|
||||||
|
export const DEBUG_GRID_DIVISIONS = 36;
|
||||||
|
export const DEBUG_GRID_PRIMARY_COLOR = "#1d4ed8";
|
||||||
|
export const DEBUG_GRID_SECONDARY_COLOR = "#1e293b";
|
||||||
|
export const DEBUG_GRID_Y = 0.01;
|
||||||
|
export const DEBUG_AXES_SIZE = 10;
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/data/dialogues.ts
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const GAME_SCENE_SKYBOX_PATH = "/skybox/sky.exr";
|
||||||
|
export const PHYSICS_SCENE_BACKGROUND_COLOR = "#0b1018";
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export const GRAB_DEFAULT_COLLIDERS = "cuboid";
|
||||||
|
export const GRAB_DEFAULT_LABEL = "Prendre";
|
||||||
|
|
||||||
|
export const GRAB_STIFFNESS_DEFAULT = 15;
|
||||||
|
export const GRAB_THROW_BOOST_DEFAULT = 1.0;
|
||||||
|
export const GRAB_HOLD_DISTANCE_DEFAULT = 2;
|
||||||
|
|
||||||
|
export const GRAB_STIFFNESS_MIN = 1;
|
||||||
|
export const GRAB_STIFFNESS_MAX = 50;
|
||||||
|
export const GRAB_STIFFNESS_STEP = 1;
|
||||||
|
|
||||||
|
export const GRAB_THROW_BOOST_MIN = 0.5;
|
||||||
|
export const GRAB_THROW_BOOST_MAX = 3.0;
|
||||||
|
export const GRAB_THROW_BOOST_STEP = 0.1;
|
||||||
|
|
||||||
|
export const GRAB_HOLD_DISTANCE_MIN = 0.5;
|
||||||
|
export const GRAB_HOLD_DISTANCE_MAX = 5.0;
|
||||||
|
export const GRAB_HOLD_DISTANCE_STEP = 0.1;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const INTERACTION_RADIUS = 3;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export const MOVE_FORWARD_KEY = "z";
|
||||||
|
export const MOVE_BACKWARD_KEY = "s";
|
||||||
|
export const MOVE_LEFT_KEY = "q";
|
||||||
|
export const MOVE_RIGHT_KEY = "d";
|
||||||
|
export const JUMP_KEY = " ";
|
||||||
|
export const INTERACT_KEY = "e";
|
||||||
|
export const PRIMARY_INTERACT_MOUSE_BUTTON = 0;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
export const AMBIENT_LIGHT_COLOR = "#dbeafe";
|
||||||
|
export const SUN_LIGHT_COLOR = "#fff7ed";
|
||||||
|
|
||||||
|
export const LIGHTING_DEFAULTS = {
|
||||||
|
ambientIntensity: 1.8,
|
||||||
|
sunIntensity: 2.8,
|
||||||
|
sunX: 60,
|
||||||
|
sunY: 80,
|
||||||
|
sunZ: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AMBIENT_INTENSITY_MIN = 0;
|
||||||
|
export const AMBIENT_INTENSITY_MAX = 5;
|
||||||
|
export const AMBIENT_INTENSITY_STEP = 0.1;
|
||||||
|
|
||||||
|
export const SUN_INTENSITY_MIN = 0;
|
||||||
|
export const SUN_INTENSITY_MAX = 8;
|
||||||
|
export const SUN_INTENSITY_STEP = 0.1;
|
||||||
|
|
||||||
|
export const SUN_X_MIN = -100;
|
||||||
|
export const SUN_X_MAX = 100;
|
||||||
|
export const SUN_X_STEP = 1;
|
||||||
|
|
||||||
|
export const SUN_Y_MIN = 0;
|
||||||
|
export const SUN_Y_MAX = 150;
|
||||||
|
export const SUN_Y_STEP = 1;
|
||||||
|
|
||||||
|
export const SUN_Z_MIN = -100;
|
||||||
|
export const SUN_Z_MAX = 100;
|
||||||
|
export const SUN_Z_STEP = 1;
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/data/missions.ts
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Vector3Tuple } from "@/types/3d";
|
||||||
|
|
||||||
|
export const PLAYER_EYE_HEIGHT = 1.75;
|
||||||
|
export const PLAYER_CAPSULE_RADIUS = 0.35;
|
||||||
|
|
||||||
|
export const PLAYER_WALK_SPEED = 11;
|
||||||
|
export const PLAYER_AIR_CONTROL_FACTOR = 0.35;
|
||||||
|
export const PLAYER_JUMP_SPEED = 9;
|
||||||
|
export const PLAYER_GRAVITY = 30;
|
||||||
|
export const PLAYER_MAX_DELTA = 0.05;
|
||||||
|
export const PLAYER_ACCELERATION_MULTIPLIER = 9;
|
||||||
|
export const PLAYER_XZ_DAMPING_FACTOR = 8;
|
||||||
|
|
||||||
|
export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [0, 100, 0];
|
||||||
|
export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Vector3Tuple } from "@/types/3d";
|
||||||
|
|
||||||
|
export const TEST_SCENE_FLOOR_POSITION: Vector3Tuple = [0, -0.5, 0];
|
||||||
|
export const TEST_SCENE_FLOOR_SIZE: Vector3Tuple = [200, 1, 200];
|
||||||
|
export const TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS: Vector3Tuple = [
|
||||||
|
100, 0.5, 100,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TEST_SCENE_GRABBABLE_POSITION: Vector3Tuple = [0, 1, -3];
|
||||||
|
export const TEST_SCENE_GRABBABLE_BOX_SIZE: Vector3Tuple = [0.5, 0.5, 0.5];
|
||||||
|
export const TEST_SCENE_GRABBABLE_COLOR = "#e07b39";
|
||||||
|
export const TEST_SCENE_GRABBABLE_ROUGHNESS = 0.6;
|
||||||
|
export const TEST_SCENE_GRABBABLE_METALNESS = 0.1;
|
||||||
|
|
||||||
|
export const TEST_SCENE_TRIGGER_POSITION: Vector3Tuple = [3, 2, -3];
|
||||||
|
export const TEST_SCENE_TRIGGER_SOUND_PATH = "/sounds/fa.mp3";
|
||||||
|
export const TEST_SCENE_TRIGGER_RADIUS = 0.4;
|
||||||
|
export const TEST_SCENE_TRIGGER_SEGMENTS = 32;
|
||||||
|
export const TEST_SCENE_TRIGGER_COLOR = "#3b82f6";
|
||||||
|
export const TEST_SCENE_TRIGGER_ROUGHNESS = 0.3;
|
||||||
|
export const TEST_SCENE_TRIGGER_METALNESS = 0.5;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Vector3Tuple } from "@/types/3d";
|
||||||
|
|
||||||
|
export const TRIGGER_DEFAULT_COLLIDERS = "ball";
|
||||||
|
export const TRIGGER_DEFAULT_LABEL = "Interagir";
|
||||||
|
export const TRIGGER_DEFAULT_SOUND_VOLUME = 1;
|
||||||
|
export const TRIGGER_DEFAULT_SPAWN_OFFSET: Vector3Tuple = [0, 0, 0];
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/data/zones.ts
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { CameraMode } from "@/types/debug";
|
||||||
|
import { useDebugStore } from "@/hooks/debug/useDebugStore";
|
||||||
|
|
||||||
|
export function useCameraMode(): CameraMode {
|
||||||
|
return useDebugStore((debug) => debug.getCameraMode());
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import type GUI from "lil-gui";
|
||||||
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
|
export function useDebugFolder(
|
||||||
|
name: string,
|
||||||
|
setup: (folder: GUI) => void,
|
||||||
|
): void {
|
||||||
|
const setupRef = useRef(setup);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setupRef.current = setup;
|
||||||
|
}, [setup]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const debug = Debug.getInstance();
|
||||||
|
if (!debug.active) return;
|
||||||
|
|
||||||
|
const folder = debug.createFolder(name);
|
||||||
|
if (folder) {
|
||||||
|
setupRef.current(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debug.destroyFolder(name);
|
||||||
|
};
|
||||||
|
}, [name]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useSyncExternalStore } from "react";
|
||||||
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
|
export function useDebugStore<T>(selector: (debug: Debug) => T): T {
|
||||||
|
const debug = Debug.getInstance();
|
||||||
|
|
||||||
|
return useSyncExternalStore(
|
||||||
|
(listener) => debug.subscribe(listener),
|
||||||
|
() => selector(debug),
|
||||||
|
() => selector(debug),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { SceneMode } from "@/types/debug";
|
||||||
|
import { useDebugStore } from "@/hooks/debug/useDebugStore";
|
||||||
|
|
||||||
|
export function useSceneMode(): SceneMode {
|
||||||
|
return useDebugStore((debug) => debug.getSceneMode());
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/hooks/useAudio.ts
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/hooks/useCinematic.ts
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/hooks/useGameState.ts
|
|
||||||
@@ -1 +1,12 @@
|
|||||||
// src/hooks/useInteraction.ts
|
import { useSyncExternalStore } from "react";
|
||||||
|
import { InteractionManager } from "@/stateManager/InteractionManager";
|
||||||
|
import type { InteractionSnapshot } from "@/types/interaction";
|
||||||
|
|
||||||
|
const manager = InteractionManager.getInstance();
|
||||||
|
|
||||||
|
export function useInteraction(): InteractionSnapshot {
|
||||||
|
return useSyncExternalStore(
|
||||||
|
manager.subscribe.bind(manager),
|
||||||
|
manager.getState.bind(manager),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
// src/hooks/useLOD.ts
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import type { RefObject } from "react";
|
||||||
|
import type { Object3D } from "three";
|
||||||
|
import { Octree } from "three/addons/math/Octree.js";
|
||||||
|
import type { OctreeReadyHandler } from "@/types/3d";
|
||||||
|
|
||||||
|
export function useOctreeGraphNode(
|
||||||
|
graphNodeRef: RefObject<Object3D | null>,
|
||||||
|
onOctreeReady: OctreeReadyHandler,
|
||||||
|
): void {
|
||||||
|
const octreeBuilt = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const graphNode = graphNodeRef.current;
|
||||||
|
if (octreeBuilt.current || !graphNode) return;
|
||||||
|
octreeBuilt.current = true;
|
||||||
|
|
||||||
|
graphNode.updateMatrixWorld(true);
|
||||||
|
|
||||||
|
const octree = new Octree();
|
||||||
|
octree.fromGraphNode(graphNode);
|
||||||
|
onOctreeReady(octree);
|
||||||
|
}, [graphNodeRef, onOctreeReady]);
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/hooks/useZoneDetection.ts
|
|
||||||
+63
-93
@@ -1,111 +1,81 @@
|
|||||||
:root {
|
:root {
|
||||||
--text: #6b6375;
|
color-scheme: dark;
|
||||||
--text-h: #08060d;
|
font-family: Inter;
|
||||||
--bg: #fff;
|
|
||||||
--border: #e5e4e7;
|
|
||||||
--code-bg: #f4f3ec;
|
|
||||||
--accent: #aa3bff;
|
|
||||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
|
||||||
--accent-border: rgba(170, 59, 255, 0.5);
|
|
||||||
--social-bg: rgba(244, 243, 236, 0.5);
|
|
||||||
--shadow:
|
|
||||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
|
||||||
|
|
||||||
--sans: system-ui, "Segoe UI", Roboto, sans-serif;
|
|
||||||
--heading: system-ui, "Segoe UI", Roboto, sans-serif;
|
|
||||||
--mono: ui-monospace, Consolas, monospace;
|
|
||||||
|
|
||||||
font: 18px/145% var(--sans);
|
|
||||||
letter-spacing: 0.18px;
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: var(--text);
|
|
||||||
background: var(--bg);
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--text: #9ca3af;
|
|
||||||
--text-h: #f3f4f6;
|
|
||||||
--bg: #16171d;
|
|
||||||
--border: #2e303a;
|
|
||||||
--code-bg: #1f2028;
|
|
||||||
--accent: #c084fc;
|
|
||||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
|
||||||
--accent-border: rgba(192, 132, 252, 0.5);
|
|
||||||
--social-bg: rgba(47, 48, 58, 0.5);
|
|
||||||
--shadow:
|
|
||||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#social .button-icon {
|
|
||||||
filter: invert(1) brightness(2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
#root {
|
#root {
|
||||||
width: 1126px;
|
margin: 0;
|
||||||
max-width: 100%;
|
width: 100vw;
|
||||||
margin: 0 auto;
|
height: 100vh;
|
||||||
text-align: center;
|
|
||||||
border-inline: 1px solid var(--border);
|
|
||||||
min-height: 100svh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
overflow: hidden;
|
||||||
|
background: #04070d;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
button,
|
||||||
h2 {
|
input,
|
||||||
font-family: var(--heading);
|
textarea,
|
||||||
font-weight: 500;
|
select {
|
||||||
color: var(--text-h);
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
canvas {
|
||||||
font-size: 56px;
|
display: block;
|
||||||
letter-spacing: -1.68px;
|
|
||||||
margin: 32px 0;
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 36px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 118%;
|
|
||||||
letter-spacing: -0.24px;
|
|
||||||
margin: 0 0 8px;
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code,
|
.crosshair {
|
||||||
.counter {
|
position: fixed;
|
||||||
font-family: var(--mono);
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border-radius: 999px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crosshair--interact {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.interact-prompt {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interact-prompt__key {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--text-h);
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
.interact-prompt__label {
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
line-height: 135%;
|
color: rgba(255, 255, 255, 0.85);
|
||||||
padding: 4px 8px;
|
letter-spacing: 0.03em;
|
||||||
background: var(--code-bg);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import "./index.css";
|
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@@ -1 +1,85 @@
|
|||||||
// src/stateManager/AudioManager.ts
|
import { logger } from "@/utils/logger";
|
||||||
|
|
||||||
|
export class AudioManager {
|
||||||
|
private static _instance: AudioManager | null = null;
|
||||||
|
private readonly _audioPools = new Map<string, HTMLAudioElement[]>();
|
||||||
|
|
||||||
|
private static readonly MAX_POOL_SIZE_PER_SOUND = 6;
|
||||||
|
private static readonly IGNORED_PLAYBACK_ERRORS = new Set([
|
||||||
|
"AbortError",
|
||||||
|
"NotAllowedError",
|
||||||
|
]);
|
||||||
|
|
||||||
|
static getInstance(): AudioManager {
|
||||||
|
if (!AudioManager._instance) {
|
||||||
|
AudioManager._instance = new AudioManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
return AudioManager._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
playSound(path: string, volume = 1): void {
|
||||||
|
const audio = this._acquireAudio(path);
|
||||||
|
audio.volume = Math.max(0, Math.min(1, volume));
|
||||||
|
audio.currentTime = 0;
|
||||||
|
|
||||||
|
void audio.play().catch((error: unknown) => {
|
||||||
|
if (
|
||||||
|
error instanceof DOMException &&
|
||||||
|
AudioManager.IGNORED_PLAYBACK_ERRORS.has(error.name)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("AudioManager", "Failed to play sound", {
|
||||||
|
path,
|
||||||
|
error: AudioManager._toLogValue(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this._audioPools.forEach((pool) => {
|
||||||
|
pool.forEach((audio) => {
|
||||||
|
audio.pause();
|
||||||
|
audio.src = "";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._audioPools.clear();
|
||||||
|
AudioManager._instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _acquireAudio(path: string): HTMLAudioElement {
|
||||||
|
const existingPool = this._audioPools.get(path);
|
||||||
|
|
||||||
|
if (existingPool) {
|
||||||
|
const availableAudio = existingPool.find(
|
||||||
|
(audio) => audio.paused || audio.ended,
|
||||||
|
);
|
||||||
|
if (availableAudio) return availableAudio;
|
||||||
|
|
||||||
|
if (existingPool.length < AudioManager.MAX_POOL_SIZE_PER_SOUND) {
|
||||||
|
const pooledAudio = new Audio(path);
|
||||||
|
existingPool.push(pooledAudio);
|
||||||
|
return pooledAudio;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recycledAudio = existingPool[0];
|
||||||
|
if (recycledAudio) return recycledAudio;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialAudio = new Audio(path);
|
||||||
|
this._audioPools.set(path, [initialAudio]);
|
||||||
|
return initialAudio;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _toLogValue(error: unknown): Error | DOMException | string {
|
||||||
|
if (error instanceof Error || error instanceof DOMException) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
// src/stateManager/CinematicManager.ts
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/stateManager/GameManager.ts
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import type {
|
||||||
|
GrabInteractableHandle,
|
||||||
|
InteractableHandle,
|
||||||
|
InteractionSnapshot,
|
||||||
|
} from "@/types/interaction";
|
||||||
|
|
||||||
|
export class InteractionManager {
|
||||||
|
private static _instance: InteractionManager | null = null;
|
||||||
|
|
||||||
|
private _focused: InteractableHandle | null = null;
|
||||||
|
private _holding = false;
|
||||||
|
private _holdingHandle: GrabInteractableHandle | null = null;
|
||||||
|
private _snapshot: InteractionSnapshot = {
|
||||||
|
focused: null,
|
||||||
|
holding: false,
|
||||||
|
};
|
||||||
|
private readonly _listeners = new Set<() => void>();
|
||||||
|
|
||||||
|
static getInstance(): InteractionManager {
|
||||||
|
if (!InteractionManager._instance) {
|
||||||
|
InteractionManager._instance = new InteractionManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
return InteractionManager._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
getState(): InteractionSnapshot {
|
||||||
|
return this._snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFocused(handle: InteractableHandle | null): void {
|
||||||
|
if (this._focused === handle) return;
|
||||||
|
if (this._holding) return;
|
||||||
|
|
||||||
|
this._focused = handle;
|
||||||
|
this._emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pressInteract(): void {
|
||||||
|
if (!this._focused) return;
|
||||||
|
|
||||||
|
if (this._focused.kind === "grab") {
|
||||||
|
this._holding = true;
|
||||||
|
this._holdingHandle = this._focused;
|
||||||
|
} else {
|
||||||
|
this._holding = false;
|
||||||
|
this._holdingHandle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._focused.onPress();
|
||||||
|
this._emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseInteract(): void {
|
||||||
|
const handle = this._holding ? this._holdingHandle : null;
|
||||||
|
if (!handle) return;
|
||||||
|
|
||||||
|
handle.onRelease();
|
||||||
|
this._holding = false;
|
||||||
|
this._holdingHandle = null;
|
||||||
|
this._emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(listener: () => void): () => void {
|
||||||
|
this._listeners.add(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this._listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this._focused = null;
|
||||||
|
this._holding = false;
|
||||||
|
this._holdingHandle = null;
|
||||||
|
this._snapshot = {
|
||||||
|
focused: null,
|
||||||
|
holding: false,
|
||||||
|
};
|
||||||
|
this._listeners.clear();
|
||||||
|
InteractionManager._instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _emit(): void {
|
||||||
|
this._snapshot = {
|
||||||
|
focused: this._focused,
|
||||||
|
holding: this._holding,
|
||||||
|
};
|
||||||
|
this._listeners.forEach((cb) => cb());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/stateManager/ZoneManager.ts
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Octree } from "three/addons/math/Octree.js";
|
||||||
|
|
||||||
|
export type Vector3Tuple = [number, number, number];
|
||||||
|
|
||||||
|
export type ColliderShape = "cuboid" | "ball" | "hull";
|
||||||
|
|
||||||
|
export type OctreeReadyHandler = (octree: Octree) => void;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export type CameraMode = "player" | "debug";
|
||||||
|
export type SceneMode = "game" | "physics";
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export type InteractableKind = "grab" | "trigger";
|
||||||
|
|
||||||
|
export interface TriggerInteractableHandle {
|
||||||
|
kind: "trigger";
|
||||||
|
label: string;
|
||||||
|
onPress: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrabInteractableHandle {
|
||||||
|
kind: "grab";
|
||||||
|
label: string;
|
||||||
|
onPress: () => void;
|
||||||
|
onRelease: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InteractableHandle =
|
||||||
|
| TriggerInteractableHandle
|
||||||
|
| GrabInteractableHandle;
|
||||||
|
|
||||||
|
export interface InteractionSnapshot {
|
||||||
|
focused: InteractableHandle | null;
|
||||||
|
holding: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||||
|
|
||||||
|
export type LogValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| Error
|
||||||
|
| DOMException
|
||||||
|
| { [key: string]: LogValue }
|
||||||
|
| LogValue[];
|
||||||
|
|
||||||
|
export type LogContext = Readonly<Record<string, LogValue>>;
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
level: LogLevel;
|
||||||
|
scope: string;
|
||||||
|
message: string;
|
||||||
|
context?: LogContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoggerConfig {
|
||||||
|
minLevel: LogLevel;
|
||||||
|
}
|
||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
declare module "three/addons/math/Capsule.js" {
|
||||||
|
import { Vector3 } from "three";
|
||||||
|
|
||||||
|
export class Capsule {
|
||||||
|
start: Vector3;
|
||||||
|
end: Vector3;
|
||||||
|
radius: number;
|
||||||
|
|
||||||
|
constructor(start?: Vector3, end?: Vector3, radius?: number);
|
||||||
|
|
||||||
|
set(start: Vector3, end: Vector3, radius: number): this;
|
||||||
|
clone(): Capsule;
|
||||||
|
copy(capsule: Capsule): this;
|
||||||
|
getCenter(target: Vector3): Vector3;
|
||||||
|
translate(v: Vector3): this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "three/addons/math/Octree.js" {
|
||||||
|
import { Object3D } from "three";
|
||||||
|
import { Capsule } from "three/addons/math/Capsule.js";
|
||||||
|
|
||||||
|
export interface CapsuleIntersectResult {
|
||||||
|
normal: import("three").Vector3;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Octree {
|
||||||
|
constructor();
|
||||||
|
fromGraphNode(group: Object3D): this;
|
||||||
|
capsuleIntersect(capsule: Capsule): CapsuleIntersectResult | false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/utils/Debug.ts
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Suspense, lazy } from "react";
|
|
||||||
const Perf = lazy(() => import("r3f-perf").then((m) => ({ default: m.Perf })));
|
|
||||||
export function DebugPerf() {
|
|
||||||
const debug = new URLSearchParams(window.location.search).has("debug");
|
|
||||||
if (!debug) return null;
|
|
||||||
return (
|
|
||||||
<Suspense fallback={null}>
|
|
||||||
<Perf position="top-left" />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/utils/Dispose.ts
|
|
||||||
@@ -1 +1,71 @@
|
|||||||
// src/utils/EventEmitter.ts
|
type Listener<TPayload> = (payload: TPayload) => void;
|
||||||
|
|
||||||
|
type ListenerMap<TEvents extends Record<string, unknown>> = {
|
||||||
|
[TKey in keyof TEvents]?: Set<Listener<TEvents[TKey]>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getListeners<
|
||||||
|
TEvents extends Record<string, unknown>,
|
||||||
|
TKey extends keyof TEvents,
|
||||||
|
>(
|
||||||
|
map: ListenerMap<TEvents>,
|
||||||
|
key: TKey,
|
||||||
|
): Set<Listener<TEvents[TKey]>> | undefined {
|
||||||
|
return map[key] as Set<Listener<TEvents[TKey]>> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventEmitter<TEvents extends Record<string, unknown>> {
|
||||||
|
private readonly listeners: ListenerMap<TEvents> = {};
|
||||||
|
|
||||||
|
on<TKey extends keyof TEvents>(
|
||||||
|
event: TKey,
|
||||||
|
listener: Listener<TEvents[TKey]>,
|
||||||
|
): () => void {
|
||||||
|
const existing = getListeners(this.listeners, event);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing.add(listener);
|
||||||
|
} else {
|
||||||
|
this.listeners[event] = new Set([listener]) as ListenerMap<TEvents>[TKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.off(event, listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
off<TKey extends keyof TEvents>(
|
||||||
|
event: TKey,
|
||||||
|
listener: Listener<TEvents[TKey]>,
|
||||||
|
): void {
|
||||||
|
const currentListeners = getListeners(this.listeners, event);
|
||||||
|
|
||||||
|
if (!currentListeners) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentListeners.delete(listener);
|
||||||
|
|
||||||
|
if (currentListeners.size === 0) {
|
||||||
|
delete this.listeners[event];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<TKey extends keyof TEvents>(event: TKey, payload: TEvents[TKey]): void {
|
||||||
|
const currentListeners = getListeners(this.listeners, event);
|
||||||
|
|
||||||
|
if (!currentListeners) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentListeners.forEach((listener) => {
|
||||||
|
listener(payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
for (const key of Object.keys(this.listeners) as (keyof TEvents)[]) {
|
||||||
|
delete this.listeners[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+50
-1
@@ -1 +1,50 @@
|
|||||||
// src/utils/Sizes.ts
|
type SizeSnapshot = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
pixelRatio: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SizeListener = (snapshot: SizeSnapshot) => void;
|
||||||
|
|
||||||
|
export class Sizes {
|
||||||
|
private snapshot: SizeSnapshot;
|
||||||
|
private readonly listeners = new Set<SizeListener>();
|
||||||
|
private readonly handleResize = (): void => {
|
||||||
|
this.snapshot = Sizes.readWindow();
|
||||||
|
this.emit();
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.snapshot = Sizes.readWindow();
|
||||||
|
window.addEventListener("resize", this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(listener: SizeListener): () => void {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getSnapshot(): SizeSnapshot {
|
||||||
|
return this.snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
window.removeEventListener("resize", this.handleResize);
|
||||||
|
this.listeners.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit(): void {
|
||||||
|
this.listeners.forEach((listener) => listener(this.snapshot));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readWindow(): SizeSnapshot {
|
||||||
|
return {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
pixelRatio: Math.min(window.devicePixelRatio, 2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+42
-1
@@ -1 +1,42 @@
|
|||||||
// src/utils/Time.ts
|
type TickListener = (delta: number, elapsed: number) => void;
|
||||||
|
|
||||||
|
export class Time {
|
||||||
|
private readonly listeners = new Set<TickListener>();
|
||||||
|
private animationFrameId = 0;
|
||||||
|
private lastTick = performance.now();
|
||||||
|
private elapsed = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.tick = this.tick.bind(this);
|
||||||
|
this.animationFrameId = window.requestAnimationFrame(this.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(listener: TickListener): () => void {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getElapsed(): number {
|
||||||
|
return this.elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
window.cancelAnimationFrame(this.animationFrameId);
|
||||||
|
this.listeners.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private tick(now: number): void {
|
||||||
|
const delta = (now - this.lastTick) / 1000;
|
||||||
|
this.lastTick = now;
|
||||||
|
this.elapsed += delta;
|
||||||
|
|
||||||
|
this.listeners.forEach((listener) => {
|
||||||
|
listener(delta, this.elapsed);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.animationFrameId = window.requestAnimationFrame(this.tick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import GUI from "lil-gui";
|
||||||
|
import type { CameraMode, SceneMode } from "@/types/debug";
|
||||||
|
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
|
||||||
|
|
||||||
|
export class Debug {
|
||||||
|
private static instance: Debug | null = null;
|
||||||
|
|
||||||
|
public readonly active: boolean;
|
||||||
|
private readonly gui: GUI | null;
|
||||||
|
private readonly folders = new Map<string, GUI>();
|
||||||
|
private readonly folderRefCounts = new Map<string, number>();
|
||||||
|
private readonly listeners = new Set<() => void>();
|
||||||
|
private readonly controls: {
|
||||||
|
cameraMode: CameraMode;
|
||||||
|
showInteractionSpheres: boolean;
|
||||||
|
sceneMode: SceneMode;
|
||||||
|
} = {
|
||||||
|
cameraMode: "player",
|
||||||
|
showInteractionSpheres: false,
|
||||||
|
sceneMode: "game",
|
||||||
|
};
|
||||||
|
|
||||||
|
static getInstance(): Debug {
|
||||||
|
if (!Debug.instance) {
|
||||||
|
Debug.instance = new Debug();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Debug.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.active = isDebugEnabled();
|
||||||
|
this.gui = this.active ? new GUI({ title: "La-Fabrik Debug" }) : null;
|
||||||
|
|
||||||
|
if (this.gui) {
|
||||||
|
const folder = this.createFolder("Debug");
|
||||||
|
|
||||||
|
if (!folder) return;
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(this.controls, "cameraMode", { Player: "player", Debug: "debug" })
|
||||||
|
.name("Camera Mode")
|
||||||
|
.onChange((value: CameraMode) => {
|
||||||
|
this.controls.cameraMode = value;
|
||||||
|
this.emit();
|
||||||
|
});
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(this.controls, "sceneMode", { Game: "game", Physics: "physics" })
|
||||||
|
.name("Scene")
|
||||||
|
.onChange((value: SceneMode) => {
|
||||||
|
this.controls.sceneMode = value;
|
||||||
|
this.emit();
|
||||||
|
});
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(this.controls, "showInteractionSpheres")
|
||||||
|
.name("Interaction Spheres")
|
||||||
|
.onChange((value: boolean) => {
|
||||||
|
this.controls.showInteractionSpheres = value;
|
||||||
|
this.emit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquires a named GUI folder. Returns the folder on first acquisition and null
|
||||||
|
* on subsequent acquisitions so callers only register controls once.
|
||||||
|
*/
|
||||||
|
createFolder(name: string): GUI | null {
|
||||||
|
if (!this.gui) return null;
|
||||||
|
|
||||||
|
const existing = this.folders.get(name);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
this.folderRefCounts.set(name, (this.folderRefCounts.get(name) ?? 0) + 1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = this.gui.addFolder(name);
|
||||||
|
this.folders.set(name, folder);
|
||||||
|
this.folderRefCounts.set(name, 1);
|
||||||
|
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyFolder(name: string): void {
|
||||||
|
const folder = this.folders.get(name);
|
||||||
|
const refCount = this.folderRefCounts.get(name);
|
||||||
|
if (!folder || refCount === undefined) return;
|
||||||
|
|
||||||
|
if (refCount > 1) {
|
||||||
|
this.folderRefCounts.set(name, refCount - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.destroy();
|
||||||
|
this.folders.delete(name);
|
||||||
|
this.folderRefCounts.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(listener: () => void): () => void {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getCameraMode(): CameraMode {
|
||||||
|
return this.controls.cameraMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSceneMode(): SceneMode {
|
||||||
|
return this.controls.sceneMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
getShowInteractionSpheres(): boolean {
|
||||||
|
return this.controls.showInteractionSpheres;
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit(): void {
|
||||||
|
this.listeners.forEach((listener) => listener());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Suspense, lazy } from "react";
|
||||||
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
|
const Perf = lazy(() => import("r3f-perf").then((m) => ({ default: m.Perf })));
|
||||||
|
|
||||||
|
export function DebugPerf(): React.JSX.Element | null {
|
||||||
|
const debug = Debug.getInstance();
|
||||||
|
|
||||||
|
if (!debug.active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<Perf position="bottom-right" />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function isDebugEnabled(): boolean {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URLSearchParams(window.location.search).has("debug");
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { OrbitControls } from "@react-three/drei";
|
||||||
|
import {
|
||||||
|
DEBUG_CAMERA_DAMPING_FACTOR,
|
||||||
|
DEBUG_CAMERA_MAX_DISTANCE,
|
||||||
|
DEBUG_CAMERA_MIN_DISTANCE,
|
||||||
|
} from "@/data/debugConfig";
|
||||||
|
import {
|
||||||
|
PLAYER_EYE_HEIGHT,
|
||||||
|
PLAYER_SPAWN_POSITION_GAME,
|
||||||
|
} from "@/data/playerConfig";
|
||||||
|
|
||||||
|
const DEBUG_CAMERA_TARGET = [
|
||||||
|
PLAYER_SPAWN_POSITION_GAME[0],
|
||||||
|
PLAYER_EYE_HEIGHT,
|
||||||
|
PLAYER_SPAWN_POSITION_GAME[2],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function DebugCameraControls(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<OrbitControls
|
||||||
|
enableDamping
|
||||||
|
dampingFactor={DEBUG_CAMERA_DAMPING_FACTOR}
|
||||||
|
minDistance={DEBUG_CAMERA_MIN_DISTANCE}
|
||||||
|
maxDistance={DEBUG_CAMERA_MAX_DISTANCE}
|
||||||
|
target={DEBUG_CAMERA_TARGET}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
DEBUG_AXES_SIZE,
|
||||||
|
DEBUG_GRID_DIVISIONS,
|
||||||
|
DEBUG_GRID_PRIMARY_COLOR,
|
||||||
|
DEBUG_GRID_SECONDARY_COLOR,
|
||||||
|
DEBUG_GRID_SIZE,
|
||||||
|
DEBUG_GRID_Y,
|
||||||
|
} from "@/data/debugConfig";
|
||||||
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
|
export function DebugHelpers(): React.JSX.Element | null {
|
||||||
|
const debug = Debug.getInstance();
|
||||||
|
|
||||||
|
if (!debug.active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<gridHelper
|
||||||
|
args={[
|
||||||
|
DEBUG_GRID_SIZE,
|
||||||
|
DEBUG_GRID_DIVISIONS,
|
||||||
|
DEBUG_GRID_PRIMARY_COLOR,
|
||||||
|
DEBUG_GRID_SECONDARY_COLOR,
|
||||||
|
]}
|
||||||
|
position={[0, DEBUG_GRID_Y, 0]}
|
||||||
|
/>
|
||||||
|
<axesHelper args={[DEBUG_AXES_SIZE]} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import type {
|
||||||
|
LogContext,
|
||||||
|
LogEntry,
|
||||||
|
LogLevel,
|
||||||
|
LoggerConfig,
|
||||||
|
} from "@/types/logger";
|
||||||
|
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
|
||||||
|
|
||||||
|
const LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||||
|
debug: 10,
|
||||||
|
info: 20,
|
||||||
|
warn: 30,
|
||||||
|
error: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LEVEL_LABELS: Record<LogLevel, string> = {
|
||||||
|
debug: "DEBUG",
|
||||||
|
info: "INFO",
|
||||||
|
warn: "WARN",
|
||||||
|
error: "ERROR",
|
||||||
|
};
|
||||||
|
|
||||||
|
const LEVEL_STYLES: Record<LogLevel, string> = {
|
||||||
|
debug: "color: #94a3b8; font-weight: 600;",
|
||||||
|
info: "color: #60a5fa; font-weight: 600;",
|
||||||
|
warn: "color: #f59e0b; font-weight: 600;",
|
||||||
|
error: "color: #f87171; font-weight: 600;",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SCOPE_STYLE = "color: #e5e7eb; font-weight: 600;";
|
||||||
|
const MESSAGE_STYLE = "color: inherit;";
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
private readonly config: LoggerConfig;
|
||||||
|
|
||||||
|
constructor(config: LoggerConfig) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(scope: string, message: string, context?: LogContext): void {
|
||||||
|
this.log("debug", scope, message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(scope: string, message: string, context?: LogContext): void {
|
||||||
|
this.log("info", scope, message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(scope: string, message: string, context?: LogContext): void {
|
||||||
|
this.log("warn", scope, message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(scope: string, message: string, context?: LogContext): void {
|
||||||
|
this.log("error", scope, message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(
|
||||||
|
level: LogLevel,
|
||||||
|
scope: string,
|
||||||
|
message: string,
|
||||||
|
context?: LogContext,
|
||||||
|
): void {
|
||||||
|
if (!this.shouldLog(level)) return;
|
||||||
|
|
||||||
|
const entry: LogEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level,
|
||||||
|
scope,
|
||||||
|
message,
|
||||||
|
...(context ? { context } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedMessage = `%c[${LEVEL_LABELS[level]}]%c [${scope}]%c ${message}`;
|
||||||
|
const args = [
|
||||||
|
formattedMessage,
|
||||||
|
LEVEL_STYLES[level],
|
||||||
|
SCOPE_STYLE,
|
||||||
|
MESSAGE_STYLE,
|
||||||
|
entry,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case "debug":
|
||||||
|
console.debug(...args);
|
||||||
|
return;
|
||||||
|
case "info":
|
||||||
|
console.info(...args);
|
||||||
|
return;
|
||||||
|
case "warn":
|
||||||
|
console.warn(...args);
|
||||||
|
return;
|
||||||
|
case "error":
|
||||||
|
console.error(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldLog(level: LogLevel): boolean {
|
||||||
|
return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.config.minLevel];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMinLevel(): LogLevel {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDebugEnabled() ? "debug" : "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = new Logger({
|
||||||
|
minLevel: resolveMinLevel(),
|
||||||
|
});
|
||||||
@@ -1 +1,18 @@
|
|||||||
// src/world/Environment.tsx
|
import { Environment as DreiEnvironment } from "@react-three/drei";
|
||||||
|
import {
|
||||||
|
GAME_SCENE_SKYBOX_PATH,
|
||||||
|
PHYSICS_SCENE_BACKGROUND_COLOR,
|
||||||
|
} from "@/data/environmentConfig";
|
||||||
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
|
|
||||||
|
export function Environment(): React.JSX.Element {
|
||||||
|
const sceneMode = useSceneMode();
|
||||||
|
|
||||||
|
if (sceneMode === "physics") {
|
||||||
|
return (
|
||||||
|
<color attach="background" args={[PHYSICS_SCENE_BACKGROUND_COLOR]} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DreiEnvironment background files={GAME_SCENE_SKYBOX_PATH} />;
|
||||||
|
}
|
||||||
|
|||||||
+105
-1
@@ -1 +1,105 @@
|
|||||||
// src/world/Lighting.tsx
|
import { useRef } from "react";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import type { AmbientLight, DirectionalLight } from "three";
|
||||||
|
import {
|
||||||
|
AMBIENT_INTENSITY_MAX,
|
||||||
|
AMBIENT_INTENSITY_MIN,
|
||||||
|
AMBIENT_INTENSITY_STEP,
|
||||||
|
AMBIENT_LIGHT_COLOR,
|
||||||
|
LIGHTING_DEFAULTS,
|
||||||
|
SUN_INTENSITY_MAX,
|
||||||
|
SUN_INTENSITY_MIN,
|
||||||
|
SUN_INTENSITY_STEP,
|
||||||
|
SUN_LIGHT_COLOR,
|
||||||
|
SUN_X_MAX,
|
||||||
|
SUN_X_MIN,
|
||||||
|
SUN_X_STEP,
|
||||||
|
SUN_Y_MAX,
|
||||||
|
SUN_Y_MIN,
|
||||||
|
SUN_Y_STEP,
|
||||||
|
SUN_Z_MAX,
|
||||||
|
SUN_Z_MIN,
|
||||||
|
SUN_Z_STEP,
|
||||||
|
} from "@/data/lightingConfig";
|
||||||
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
|
|
||||||
|
type LightingState = {
|
||||||
|
ambientIntensity: number;
|
||||||
|
sunIntensity: number;
|
||||||
|
sunX: number;
|
||||||
|
sunY: number;
|
||||||
|
sunZ: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LIGHTING_STATE: LightingState = { ...LIGHTING_DEFAULTS };
|
||||||
|
|
||||||
|
export function Lighting(): React.JSX.Element {
|
||||||
|
const ambient = useRef<AmbientLight>(null);
|
||||||
|
const sun = useRef<DirectionalLight>(null);
|
||||||
|
|
||||||
|
useDebugFolder("Lighting", (folder) => {
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
LIGHTING_STATE,
|
||||||
|
"ambientIntensity",
|
||||||
|
AMBIENT_INTENSITY_MIN,
|
||||||
|
AMBIENT_INTENSITY_MAX,
|
||||||
|
AMBIENT_INTENSITY_STEP,
|
||||||
|
)
|
||||||
|
.name("Ambient");
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
LIGHTING_STATE,
|
||||||
|
"sunIntensity",
|
||||||
|
SUN_INTENSITY_MIN,
|
||||||
|
SUN_INTENSITY_MAX,
|
||||||
|
SUN_INTENSITY_STEP,
|
||||||
|
)
|
||||||
|
.name("Sun Intensity");
|
||||||
|
folder
|
||||||
|
.add(LIGHTING_STATE, "sunX", SUN_X_MIN, SUN_X_MAX, SUN_X_STEP)
|
||||||
|
.name("Sun X");
|
||||||
|
folder
|
||||||
|
.add(LIGHTING_STATE, "sunY", SUN_Y_MIN, SUN_Y_MAX, SUN_Y_STEP)
|
||||||
|
.name("Sun Y");
|
||||||
|
folder
|
||||||
|
.add(LIGHTING_STATE, "sunZ", SUN_Z_MIN, SUN_Z_MAX, SUN_Z_STEP)
|
||||||
|
.name("Sun Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (ambient.current) {
|
||||||
|
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sun.current) {
|
||||||
|
sun.current.position.set(
|
||||||
|
LIGHTING_STATE.sunX,
|
||||||
|
LIGHTING_STATE.sunY,
|
||||||
|
LIGHTING_STATE.sunZ,
|
||||||
|
);
|
||||||
|
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ambientLight
|
||||||
|
ref={ambient}
|
||||||
|
intensity={LIGHTING_STATE.ambientIntensity}
|
||||||
|
color={AMBIENT_LIGHT_COLOR}
|
||||||
|
/>
|
||||||
|
<directionalLight
|
||||||
|
ref={sun}
|
||||||
|
position={[
|
||||||
|
LIGHTING_STATE.sunX,
|
||||||
|
LIGHTING_STATE.sunY,
|
||||||
|
LIGHTING_STATE.sunZ,
|
||||||
|
]}
|
||||||
|
intensity={LIGHTING_STATE.sunIntensity}
|
||||||
|
color={SUN_LIGHT_COLOR}
|
||||||
|
castShadow
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+55
-1
@@ -1 +1,55 @@
|
|||||||
// src/world/Map.tsx
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { MAP_DEBUG_BOX_HELPER_COLOR } from "@/data/debugConfig";
|
||||||
|
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
||||||
|
import type { OctreeReadyHandler } from "@/types/3d";
|
||||||
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
|
const MAP_PATH = "/models/map/model.gltf";
|
||||||
|
|
||||||
|
interface MapProps {
|
||||||
|
onOctreeReady: OctreeReadyHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Map({ onOctreeReady }: MapProps): React.JSX.Element {
|
||||||
|
const { scene: gltfScene } = useGLTF(MAP_PATH);
|
||||||
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
const boxHelpersRef = useRef<THREE.BoxHelper[]>([]);
|
||||||
|
const { scene } = useThree();
|
||||||
|
|
||||||
|
useOctreeGraphNode(groupRef, onOctreeReady);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const debug = Debug.getInstance();
|
||||||
|
if (!debug.active || !groupRef.current) return;
|
||||||
|
|
||||||
|
const helpers: THREE.BoxHelper[] = [];
|
||||||
|
|
||||||
|
groupRef.current.traverse((child) => {
|
||||||
|
if (!(child instanceof THREE.Mesh)) return;
|
||||||
|
const helper = new THREE.BoxHelper(child, MAP_DEBUG_BOX_HELPER_COLOR);
|
||||||
|
scene.add(helper);
|
||||||
|
helpers.push(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
boxHelpersRef.current = helpers;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
helpers.forEach((h) => {
|
||||||
|
scene.remove(h);
|
||||||
|
h.dispose();
|
||||||
|
});
|
||||||
|
boxHelpersRef.current = [];
|
||||||
|
};
|
||||||
|
}, [scene]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group ref={groupRef}>
|
||||||
|
<primitive object={gltfScene} />
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useGLTF.preload(MAP_PATH);
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
// src/world/PostFX.tsx
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import type { Octree } from "three/addons/math/Octree.js";
|
||||||
|
import {
|
||||||
|
PLAYER_SPAWN_POSITION_GAME,
|
||||||
|
PLAYER_SPAWN_POSITION_PHYSICS,
|
||||||
|
} from "@/data/playerConfig";
|
||||||
|
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
|
import { DebugCameraControls } from "@/utils/debug/scene/DebugCameraControls";
|
||||||
|
import { DebugHelpers } from "@/utils/debug/scene/DebugHelpers";
|
||||||
|
import { Environment } from "@/world/Environment";
|
||||||
|
import { Lighting } from "@/world/Lighting";
|
||||||
|
import { Map } from "@/world/Map";
|
||||||
|
import { PlayerComponent } from "@/world/player/PlayerComponent";
|
||||||
|
import { TestScene } from "@/world/debug/TestScene";
|
||||||
|
|
||||||
|
export function World(): React.JSX.Element {
|
||||||
|
const cameraMode = useCameraMode();
|
||||||
|
const sceneMode = useSceneMode();
|
||||||
|
const [octree, setOctree] = useState<Octree | null>(null);
|
||||||
|
const playerSpawnPosition =
|
||||||
|
sceneMode === "game"
|
||||||
|
? PLAYER_SPAWN_POSITION_GAME
|
||||||
|
: PLAYER_SPAWN_POSITION_PHYSICS;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Environment />
|
||||||
|
<Lighting />
|
||||||
|
<DebugHelpers />
|
||||||
|
{cameraMode === "debug" ? <DebugCameraControls /> : null}
|
||||||
|
|
||||||
|
{sceneMode === "game" ? (
|
||||||
|
<Map onOctreeReady={setOctree} />
|
||||||
|
) : (
|
||||||
|
<TestScene onOctreeReady={setOctree} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cameraMode !== "debug" ? (
|
||||||
|
<PlayerComponent octree={octree} spawnPosition={playerSpawnPosition} />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { GrabbableObject } from "@/components/3d/GrabbableObject";
|
||||||
|
import { TriggerObject } from "@/components/3d/TriggerObject";
|
||||||
|
import {
|
||||||
|
TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS,
|
||||||
|
TEST_SCENE_FLOOR_POSITION,
|
||||||
|
TEST_SCENE_FLOOR_SIZE,
|
||||||
|
TEST_SCENE_GRABBABLE_BOX_SIZE,
|
||||||
|
TEST_SCENE_GRABBABLE_COLOR,
|
||||||
|
TEST_SCENE_GRABBABLE_METALNESS,
|
||||||
|
TEST_SCENE_GRABBABLE_POSITION,
|
||||||
|
TEST_SCENE_GRABBABLE_ROUGHNESS,
|
||||||
|
TEST_SCENE_TRIGGER_COLOR,
|
||||||
|
TEST_SCENE_TRIGGER_METALNESS,
|
||||||
|
TEST_SCENE_TRIGGER_POSITION,
|
||||||
|
TEST_SCENE_TRIGGER_RADIUS,
|
||||||
|
TEST_SCENE_TRIGGER_ROUGHNESS,
|
||||||
|
TEST_SCENE_TRIGGER_SEGMENTS,
|
||||||
|
TEST_SCENE_TRIGGER_SOUND_PATH,
|
||||||
|
} from "@/data/testSceneConfig";
|
||||||
|
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
||||||
|
import type { OctreeReadyHandler } from "@/types/3d";
|
||||||
|
|
||||||
|
interface TestSceneProps {
|
||||||
|
onOctreeReady: OctreeReadyHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TestScene({
|
||||||
|
onOctreeReady,
|
||||||
|
}: TestSceneProps): React.JSX.Element {
|
||||||
|
const floorRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
|
useOctreeGraphNode(floorRef, onOctreeReady);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<group ref={floorRef}>
|
||||||
|
<mesh visible={false} position={TEST_SCENE_FLOOR_POSITION}>
|
||||||
|
<boxGeometry args={TEST_SCENE_FLOOR_SIZE} />
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<Physics>
|
||||||
|
<RigidBody type="fixed">
|
||||||
|
<CuboidCollider
|
||||||
|
args={TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS}
|
||||||
|
position={TEST_SCENE_FLOOR_POSITION}
|
||||||
|
/>
|
||||||
|
</RigidBody>
|
||||||
|
|
||||||
|
<GrabbableObject
|
||||||
|
position={TEST_SCENE_GRABBABLE_POSITION}
|
||||||
|
colliders="cuboid"
|
||||||
|
>
|
||||||
|
<mesh castShadow receiveShadow>
|
||||||
|
<boxGeometry args={TEST_SCENE_GRABBABLE_BOX_SIZE} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={TEST_SCENE_GRABBABLE_COLOR}
|
||||||
|
roughness={TEST_SCENE_GRABBABLE_ROUGHNESS}
|
||||||
|
metalness={TEST_SCENE_GRABBABLE_METALNESS}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</GrabbableObject>
|
||||||
|
|
||||||
|
<TriggerObject
|
||||||
|
position={TEST_SCENE_TRIGGER_POSITION}
|
||||||
|
soundPath={TEST_SCENE_TRIGGER_SOUND_PATH}
|
||||||
|
>
|
||||||
|
<mesh castShadow receiveShadow>
|
||||||
|
<sphereGeometry
|
||||||
|
args={[
|
||||||
|
TEST_SCENE_TRIGGER_RADIUS,
|
||||||
|
TEST_SCENE_TRIGGER_SEGMENTS,
|
||||||
|
TEST_SCENE_TRIGGER_SEGMENTS,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={TEST_SCENE_TRIGGER_COLOR}
|
||||||
|
roughness={TEST_SCENE_TRIGGER_ROUGHNESS}
|
||||||
|
metalness={TEST_SCENE_TRIGGER_METALNESS}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</TriggerObject>
|
||||||
|
</Physics>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/world/player/Crosshair.tsx
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/world/player/FPSController.tsx
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { PointerLockControls } from "@react-three/drei";
|
||||||
|
|
||||||
|
export function PlayerCamera(): React.JSX.Element {
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
document.exitPointerLock();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <PointerLockControls />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import type { Octree } from "three/addons/math/Octree.js";
|
||||||
|
import type { Vector3Tuple } from "@/types/3d";
|
||||||
|
import { PlayerCamera } from "@/world/player/PlayerCamera";
|
||||||
|
import { PlayerController } from "@/world/player/PlayerController";
|
||||||
|
|
||||||
|
interface PlayerComponentProps {
|
||||||
|
octree: Octree | null;
|
||||||
|
spawnPosition: Vector3Tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlayerComponent({
|
||||||
|
spawnPosition,
|
||||||
|
octree,
|
||||||
|
}: PlayerComponentProps): React.JSX.Element {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
camera.position.set(...spawnPosition);
|
||||||
|
}, [camera, spawnPosition]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PlayerCamera />
|
||||||
|
<PlayerController octree={octree} spawnPosition={spawnPosition} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { Capsule } from "three/addons/math/Capsule.js";
|
||||||
|
import type { Octree } from "three/addons/math/Octree.js";
|
||||||
|
import {
|
||||||
|
INTERACT_KEY,
|
||||||
|
JUMP_KEY,
|
||||||
|
MOVE_BACKWARD_KEY,
|
||||||
|
MOVE_FORWARD_KEY,
|
||||||
|
MOVE_LEFT_KEY,
|
||||||
|
MOVE_RIGHT_KEY,
|
||||||
|
PRIMARY_INTERACT_MOUSE_BUTTON,
|
||||||
|
} from "@/data/keybindings";
|
||||||
|
import {
|
||||||
|
PLAYER_ACCELERATION_MULTIPLIER,
|
||||||
|
PLAYER_AIR_CONTROL_FACTOR,
|
||||||
|
PLAYER_CAPSULE_RADIUS,
|
||||||
|
PLAYER_EYE_HEIGHT,
|
||||||
|
PLAYER_GRAVITY,
|
||||||
|
PLAYER_JUMP_SPEED,
|
||||||
|
PLAYER_MAX_DELTA,
|
||||||
|
PLAYER_WALK_SPEED,
|
||||||
|
PLAYER_XZ_DAMPING_FACTOR,
|
||||||
|
} from "@/data/playerConfig";
|
||||||
|
import { InteractionManager } from "@/stateManager/InteractionManager";
|
||||||
|
import type { Vector3Tuple } from "@/types/3d";
|
||||||
|
|
||||||
|
type Keys = {
|
||||||
|
forward: boolean;
|
||||||
|
backward: boolean;
|
||||||
|
left: boolean;
|
||||||
|
right: boolean;
|
||||||
|
jump: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_KEYS: Keys = {
|
||||||
|
forward: false,
|
||||||
|
backward: false,
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
jump: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PlayerControllerProps {
|
||||||
|
octree: Octree | null;
|
||||||
|
spawnPosition: Vector3Tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _forward = new THREE.Vector3();
|
||||||
|
const _right = new THREE.Vector3();
|
||||||
|
const _wishDir = new THREE.Vector3();
|
||||||
|
const _up = new THREE.Vector3(0, 1, 0);
|
||||||
|
const _translateVec = new THREE.Vector3();
|
||||||
|
const _collisionCorrection = new THREE.Vector3();
|
||||||
|
|
||||||
|
export function PlayerController({
|
||||||
|
octree,
|
||||||
|
spawnPosition,
|
||||||
|
}: PlayerControllerProps): null {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
const keys = useRef<Keys>({ ...DEFAULT_KEYS });
|
||||||
|
const velocity = useRef(new THREE.Vector3());
|
||||||
|
const onFloor = useRef(false);
|
||||||
|
const wantsJump = useRef(false);
|
||||||
|
|
||||||
|
const capsule = useRef(
|
||||||
|
new Capsule(
|
||||||
|
new THREE.Vector3(0, PLAYER_CAPSULE_RADIUS, 0),
|
||||||
|
new THREE.Vector3(0, PLAYER_EYE_HEIGHT - PLAYER_CAPSULE_RADIUS, 0),
|
||||||
|
PLAYER_CAPSULE_RADIUS,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
capsule.current.start.set(
|
||||||
|
spawnPosition[0],
|
||||||
|
spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS,
|
||||||
|
spawnPosition[2],
|
||||||
|
);
|
||||||
|
capsule.current.end.set(...spawnPosition);
|
||||||
|
velocity.current.set(0, 0, 0);
|
||||||
|
onFloor.current = false;
|
||||||
|
wantsJump.current = false;
|
||||||
|
camera.position.copy(capsule.current.end);
|
||||||
|
}, [camera, spawnPosition]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interaction = InteractionManager.getInstance();
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||||
|
switch (event.key.toLowerCase()) {
|
||||||
|
case MOVE_FORWARD_KEY:
|
||||||
|
keys.current.forward = true;
|
||||||
|
break;
|
||||||
|
case MOVE_BACKWARD_KEY:
|
||||||
|
keys.current.backward = true;
|
||||||
|
break;
|
||||||
|
case MOVE_LEFT_KEY:
|
||||||
|
keys.current.left = true;
|
||||||
|
break;
|
||||||
|
case MOVE_RIGHT_KEY:
|
||||||
|
keys.current.right = true;
|
||||||
|
break;
|
||||||
|
case JUMP_KEY:
|
||||||
|
wantsJump.current = true;
|
||||||
|
break;
|
||||||
|
case INTERACT_KEY:
|
||||||
|
if (interaction.getState().focused?.kind === "trigger") {
|
||||||
|
interaction.pressInteract();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyUp = (event: KeyboardEvent): void => {
|
||||||
|
switch (event.key.toLowerCase()) {
|
||||||
|
case MOVE_FORWARD_KEY:
|
||||||
|
keys.current.forward = false;
|
||||||
|
break;
|
||||||
|
case MOVE_BACKWARD_KEY:
|
||||||
|
keys.current.backward = false;
|
||||||
|
break;
|
||||||
|
case MOVE_LEFT_KEY:
|
||||||
|
keys.current.left = false;
|
||||||
|
break;
|
||||||
|
case MOVE_RIGHT_KEY:
|
||||||
|
keys.current.right = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (event: MouseEvent): void => {
|
||||||
|
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
|
||||||
|
if (interaction.getState().focused?.kind === "grab") {
|
||||||
|
interaction.pressInteract();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = (event: MouseEvent): void => {
|
||||||
|
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
|
||||||
|
if (interaction.getState().holding) {
|
||||||
|
interaction.releaseInteract();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
window.addEventListener("keyup", handleKeyUp);
|
||||||
|
document.addEventListener("mousedown", handleMouseDown);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
window.removeEventListener("keyup", handleKeyUp);
|
||||||
|
document.removeEventListener("mousedown", handleMouseDown);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
keys.current = { ...DEFAULT_KEYS };
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
const dt = Math.min(delta, PLAYER_MAX_DELTA);
|
||||||
|
|
||||||
|
camera.getWorldDirection(_forward);
|
||||||
|
_forward.setY(0);
|
||||||
|
if (_forward.lengthSq() > 0) {
|
||||||
|
_forward.normalize();
|
||||||
|
_right.crossVectors(_forward, _up).normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
_wishDir.set(0, 0, 0);
|
||||||
|
if (keys.current.forward) _wishDir.add(_forward);
|
||||||
|
if (keys.current.backward) _wishDir.sub(_forward);
|
||||||
|
if (keys.current.left) _wishDir.sub(_right);
|
||||||
|
if (keys.current.right) _wishDir.add(_right);
|
||||||
|
if (_wishDir.lengthSq() > 0) _wishDir.normalize();
|
||||||
|
|
||||||
|
const accel = onFloor.current
|
||||||
|
? PLAYER_WALK_SPEED
|
||||||
|
: PLAYER_WALK_SPEED * PLAYER_AIR_CONTROL_FACTOR;
|
||||||
|
velocity.current.x +=
|
||||||
|
_wishDir.x * accel * dt * PLAYER_ACCELERATION_MULTIPLIER;
|
||||||
|
velocity.current.z +=
|
||||||
|
_wishDir.z * accel * dt * PLAYER_ACCELERATION_MULTIPLIER;
|
||||||
|
|
||||||
|
const damping = Math.exp(-PLAYER_XZ_DAMPING_FACTOR * dt);
|
||||||
|
velocity.current.x *= damping;
|
||||||
|
velocity.current.z *= damping;
|
||||||
|
|
||||||
|
if (onFloor.current) {
|
||||||
|
velocity.current.y = Math.max(0, velocity.current.y);
|
||||||
|
if (wantsJump.current) {
|
||||||
|
velocity.current.y = PLAYER_JUMP_SPEED;
|
||||||
|
onFloor.current = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
velocity.current.y -= PLAYER_GRAVITY * dt;
|
||||||
|
}
|
||||||
|
wantsJump.current = false;
|
||||||
|
|
||||||
|
_translateVec.copy(velocity.current).multiplyScalar(dt);
|
||||||
|
capsule.current.translate(_translateVec);
|
||||||
|
|
||||||
|
if (octree) {
|
||||||
|
const result = octree.capsuleIntersect(capsule.current);
|
||||||
|
onFloor.current = false;
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
onFloor.current = result.normal.y > 0;
|
||||||
|
|
||||||
|
if (!onFloor.current) {
|
||||||
|
const vn = result.normal.dot(velocity.current);
|
||||||
|
velocity.current.addScaledVector(result.normal, -vn);
|
||||||
|
} else {
|
||||||
|
velocity.current.y = Math.max(0, velocity.current.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule.current.translate(
|
||||||
|
_collisionCorrection.copy(result.normal).multiplyScalar(result.depth),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
camera.position.copy(capsule.current.end);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/world/zones/FarmZone.tsx
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// src/world/zones/PowerGridZone.tsx
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user