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
|
||||
```
|
||||
|
||||
The free debug camera is toggled from the debug panel, not mounted permanently.
|
||||
|
||||
## Debug singleton
|
||||
|
||||
```ts
|
||||
// src/utils/Debug.ts
|
||||
// src/utils/debug/Debug.ts
|
||||
import GUI from "lil-gui";
|
||||
|
||||
export class Debug {
|
||||
@@ -56,14 +58,15 @@ if (debug.active) {
|
||||
r3f-perf is loaded only in debug mode to avoid dependency issues in production:
|
||||
|
||||
```tsx
|
||||
// src/components/3d/DebugPerf.tsx
|
||||
// src/utils/debug/DebugPerf.tsx
|
||||
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() {
|
||||
const debug = new URLSearchParams(window.location.search).has("debug");
|
||||
if (!debug) return null;
|
||||
const debug = Debug.getInstance();
|
||||
if (!debug.active) return null;
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
|
||||
@@ -3,8 +3,12 @@ name: 🔍 Lint
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches: [develop, main]
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -71,6 +75,8 @@ jobs:
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: 🧰 Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
@@ -3,8 +3,12 @@ name: 📊 Quality
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches: [develop, main]
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
security:
|
||||
@@ -53,6 +57,8 @@ jobs:
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: 🧰 Setup Node
|
||||
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
|
||||
|
||||
| Package | Doc |
|
||||
| -------------------------------------------------- | ------------------------------------ |
|
||||
| [TypeScript](https://www.typescriptlang.org/docs/) | https://www.typescriptlang.org/docs/ |
|
||||
| [React](https://react.dev/learn) | https://react.dev/learn |
|
||||
| [Vite](https://vite.dev/guide/) | https://vite.dev/guide/ |
|
||||
| [ESLint](https://eslint.org/docs/latest/) | https://eslint.org/docs/latest/ |
|
||||
| [Prettier](https://prettier.io/docs/) | https://prettier.io/docs/ |
|
||||
| Package |
|
||||
| -------------------------------------------------- |
|
||||
| [TypeScript](https://www.typescriptlang.org/docs/) |
|
||||
| [React](https://react.dev/learn) |
|
||||
| [Vite](https://vite.dev/guide/) |
|
||||
| [ESLint](https://eslint.org/docs/latest/) |
|
||||
| [Prettier](https://prettier.io/docs/) |
|
||||
|
||||
### 3D Engine
|
||||
|
||||
| Package | Doc |
|
||||
| ----------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
||||
| [Three.js](https://threejs.org/docs/) | https://threejs.org/docs/ |
|
||||
| [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) | https://docs.pmnd.rs/react-three-fiber |
|
||||
| [@react-three/drei](https://pmndrs.github.io/drei) | https://pmndrs.github.io/drei |
|
||||
| [@react-three/rapier](https://rapier.rs/docs/) | https://rapier.rs/docs/user_guides/javascript/ |
|
||||
| [@react-three/postprocessing](https://github.com/pmndrs/postprocessing) | https://github.com/pmndrs/postprocessing |
|
||||
| [GSAP](https://gsap.com/docs/v3/Installation/) | https://gsap.com/docs/v3/ |
|
||||
| Package |
|
||||
| ----------------------------------------------------------------------------------------- |
|
||||
| [Three.js](https://threejs.org/docs/) |
|
||||
| [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) |
|
||||
| [@react-three/drei](https://pmndrs.github.io/drei) |
|
||||
| [@react-three/rapier](https://rapier.rs/docs/) |
|
||||
| [@react-three/postprocessing](https://github.com/pmndrs/postprocessing) |
|
||||
| [GSAP](https://gsap.com/docs/v3/Installation/) |
|
||||
|
||||
### Performance & Effects
|
||||
|
||||
| Package | Doc |
|
||||
| --------------------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [r3f-perf](https://github.com/utsuboco/r3f-perf) | https://github.com/utsuboco/r3f-perf |
|
||||
| [AnimationMixer](https://threejs.org/docs/#api/en/animation/AnimationMixer) | https://threejs.org/docs/#api/en/animation/AnimationMixer |
|
||||
| Package |
|
||||
| --------------------------------------------------------------------------- |
|
||||
| [r3f-perf](https://github.com/utsuboco/r3f-perf) |
|
||||
| [AnimationMixer](https://threejs.org/docs/#api/en/animation/AnimationMixer) |
|
||||
|
||||
## 🗂 Project Structure
|
||||
|
||||
@@ -49,8 +49,9 @@ la-fabrik/
|
||||
│
|
||||
└── src/
|
||||
├── world/ # Single persistent 3D world
|
||||
│ ├── World.tsx # Main scene composition
|
||||
│ ├── Map.tsx # Base map, always mounted
|
||||
│ ├── Lighting.tsx # Ambient, directional, point lights
|
||||
│ ├── Lighting.tsx # Ambient, directional, point lights
|
||||
│ ├── Environment.tsx # HDRI, fog, sky
|
||||
│ ├── PostFX.tsx # Bloom, SSAO, chromatic aberration
|
||||
│ ├── zones/ # Spatial zones — LOD per zone
|
||||
@@ -98,11 +99,24 @@ la-fabrik/
|
||||
│ └── fragment.glsl
|
||||
│
|
||||
├── utils/
|
||||
│ ├── Debug.ts # lil-gui panel
|
||||
│ ├── EventEmitter.ts # Simple pub/sub for manager-to-manager events
|
||||
│ └── Dispose.ts # traverse() + dispose() helper
|
||||
│ ├── EventEmitter.ts # Simple typed pub/sub utility
|
||||
│ ├── Sizes.ts # Viewport size tracking
|
||||
│ ├── 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
|
||||
```
|
||||
|
||||
@@ -115,8 +129,8 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open `http://localhost:5173` — standard experience.
|
||||
Open `http://localhost:5173?debug` — debug panel + r3f-perf overlay.
|
||||
- app: `http://localhost:5173`
|
||||
- debug mode: `http://localhost:5173?debug`
|
||||
|
||||
## 📜 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
|
||||
- **Declarative React components** for all 3D scene objects
|
||||
## Runtime Structure
|
||||
|
||||
This distinction is intentional. Scene elements such as the map, lights, environment, zones, and player are implemented as **React Three Fiber components** and mounted through `<Canvas>`.
|
||||
Global systems such as gameplay flow, cinematics, audio, and debug tooling are implemented as **manager classes**.
|
||||
- `src/App.tsx` mounts the `Canvas`, the 3D `World`, the debug perf overlay, and the HTML overlays.
|
||||
- `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`
|
||||
- `CinematicManager`
|
||||
- `AudioManager`
|
||||
- `ZoneManager`
|
||||
- `Debug`
|
||||
- `EventEmitter`
|
||||
- Debug mode is enabled with `?debug`.
|
||||
- `src/utils/debug/Debug.ts` owns the `lil-gui` instance and debug controls.
|
||||
- `src/hooks/debug/useCameraMode.ts` and `src/hooks/debug/useSceneMode.ts` subscribe to debug state.
|
||||
- `src/utils/debug/DebugPerf.tsx` lazily mounts `r3f-perf` in debug mode.
|
||||
- `src/utils/debug/scene/DebugHelpers.tsx` mounts debug helpers.
|
||||
- `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
|
||||
// stateManager/GameManager.ts
|
||||
export class GameManager {
|
||||
private static _instance: GameManager | null = null;
|
||||
|
||||
cinematic!: CinematicManager;
|
||||
audio!: AudioManager;
|
||||
zone!: ZoneManager;
|
||||
|
||||
static getInstance(): GameManager {
|
||||
if (!GameManager._instance) {
|
||||
GameManager._instance = new GameManager();
|
||||
}
|
||||
return GameManager._instance;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
this.cinematic = CinematicManager.getInstance();
|
||||
this.audio = AudioManager.getInstance();
|
||||
this.zone = ZoneManager.getInstance();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.cinematic.destroy();
|
||||
this.audio.destroy();
|
||||
this.zone.destroy();
|
||||
GameManager._instance = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```ts
|
||||
const game = GameManager.getInstance();
|
||||
game.startMission("workshop");
|
||||
```
|
||||
|
||||
**Important:** scene objects such as `Map`, `WorkshopZone`, `Lighting`, or `Environment` are **not** singletons and must remain standard React components.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scene Objects Are React Components, Not Manager Classes
|
||||
|
||||
All 3D scene objects are implemented as **declarative React components**.
|
||||
|
||||
This includes:
|
||||
|
||||
- maps
|
||||
- lights
|
||||
- environments
|
||||
- player controllers
|
||||
- zones
|
||||
- interactive props
|
||||
- postprocessing layers
|
||||
|
||||
This keeps the code aligned with the R3F runtime instead of rebuilding a parallel imperative engine.
|
||||
|
||||
Example:
|
||||
|
||||
```tsx
|
||||
// world/zones/WorkshopZone.tsx
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
|
||||
export function WorkshopZone() {
|
||||
const root = useRef<THREE.Group>(null);
|
||||
const gltf = useGLTF("/models/workshop/ebike.glb");
|
||||
const mixer = useRef<THREE.AnimationMixer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
mixer.current = new THREE.AnimationMixer(gltf.scene);
|
||||
|
||||
return () => {
|
||||
mixer.current?.stopAllAction();
|
||||
mixer.current = null;
|
||||
};
|
||||
}, [gltf.scene]);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
mixer.current?.update(delta);
|
||||
});
|
||||
|
||||
return <primitive ref={root} object={gltf.scene.clone()} />;
|
||||
}
|
||||
```
|
||||
|
||||
Per-frame values such as movement, interpolation, camera smoothing, and physics must stay in:
|
||||
|
||||
- `useRef`
|
||||
- `useFrame`
|
||||
- Rapier bodies
|
||||
- other frame-based systems
|
||||
|
||||
They must **never** go through React state.
|
||||
|
||||
---
|
||||
|
||||
## 3. Single Source of Truth for Durable Gameplay State
|
||||
|
||||
The project uses a single authoritative `GameManager` for durable gameplay state.
|
||||
|
||||
React components subscribe to that state through thin hooks.
|
||||
Other managers communicate through `GameManager`, which acts as the main gameplay orchestrator.
|
||||
|
||||
High-frequency values such as movement, camera interpolation, or physics never go through React state and stay in refs or frame-based systems.
|
||||
|
||||
```ts
|
||||
// stateManager/GameManager.ts
|
||||
type Phase = "loading" | "intro" | "exploring" | "cinematic" | "outro";
|
||||
type ZoneId = "workshop" | "powerGrid" | "farm" | null;
|
||||
|
||||
type GameSnapshot = {
|
||||
phase: Phase;
|
||||
activeZone: ZoneId;
|
||||
missionId: string | null;
|
||||
missionStep: number;
|
||||
inputLocked: boolean;
|
||||
dialogueId: string | null;
|
||||
};
|
||||
|
||||
export class GameManager {
|
||||
private static _instance: GameManager | null = null;
|
||||
private listeners = new Set<() => void>();
|
||||
|
||||
private state: GameSnapshot = {
|
||||
phase: "loading",
|
||||
activeZone: null,
|
||||
missionId: null,
|
||||
missionStep: 0,
|
||||
inputLocked: false,
|
||||
dialogueId: null,
|
||||
};
|
||||
|
||||
static getInstance(): GameManager {
|
||||
if (!GameManager._instance) {
|
||||
GameManager._instance = new GameManager();
|
||||
}
|
||||
return GameManager._instance;
|
||||
}
|
||||
|
||||
getState(): GameSnapshot {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
private emit(): void {
|
||||
this.listeners.forEach((cb) => cb());
|
||||
}
|
||||
|
||||
setPhase(phase: Phase): void {
|
||||
this.state.phase = phase;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
setActiveZone(zone: ZoneId): void {
|
||||
this.state.activeZone = zone;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
startMission(id: string): void {
|
||||
this.state.missionId = id;
|
||||
this.state.missionStep = 0;
|
||||
this.emit();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// hooks/useGameState.ts
|
||||
import { useEffect, useState } from "react";
|
||||
import { GameManager } from "@/stateManager/GameManager";
|
||||
|
||||
export function useGameState() {
|
||||
const game = GameManager.getInstance();
|
||||
const [state, setState] = useState(game.getState());
|
||||
|
||||
useEffect(() => {
|
||||
return game.subscribe(() => {
|
||||
setState({ ...game.getState() });
|
||||
});
|
||||
}, [game]);
|
||||
|
||||
return state;
|
||||
}
|
||||
```
|
||||
|
||||
This keeps the architecture simple:
|
||||
|
||||
- **GameManager** owns durable gameplay state
|
||||
- **other managers** handle side effects
|
||||
- **React components** render that state
|
||||
- **R3F frame systems** handle fast-changing values
|
||||
|
||||
---
|
||||
|
||||
## 4. Side Effects Stay in Specialized Managers
|
||||
|
||||
Managers other than `GameManager` should not become secondary state stores.
|
||||
|
||||
Their role is to manage side effects and specialized runtime logic, such as:
|
||||
|
||||
- GSAP timelines
|
||||
- audio playback
|
||||
- zone entry detection
|
||||
- interaction triggers
|
||||
- camera lock/unlock
|
||||
- temporary event coordination
|
||||
|
||||
They can read from `GameManager`, react to its state, or notify it of important transitions.
|
||||
|
||||
Example flow:
|
||||
|
||||
```
|
||||
Component / Hook
|
||||
↓
|
||||
GameManager.getInstance()
|
||||
├── startMission('workshop')
|
||||
├── cinematic.play('intro_workshop')
|
||||
├── audio.playAmbience('workshop')
|
||||
└── zone.setActive('workshop')
|
||||
```
|
||||
|
||||
This keeps the dependency graph understandable while avoiding duplicated durable state.
|
||||
|
||||
---
|
||||
|
||||
## 5. Memory Management — Dispose Only What You Own
|
||||
|
||||
GPU memory must be cleaned carefully.
|
||||
|
||||
However, the project does **not** blindly deep-dispose every object on unmount.
|
||||
Only resources explicitly created and owned by the current component or manager should be disposed.
|
||||
|
||||
This includes things like:
|
||||
|
||||
- custom materials
|
||||
- render targets
|
||||
- postprocessing passes
|
||||
- manually created geometries
|
||||
- manually created textures
|
||||
- temporary clones with owned resources
|
||||
|
||||
Shared or cached assets must **not** be blindly disposed.
|
||||
|
||||
```ts
|
||||
// utils/Dispose.ts
|
||||
import * as THREE from "three";
|
||||
|
||||
export class Dispose {
|
||||
static material(material: THREE.Material): void {
|
||||
for (const value of Object.values(material)) {
|
||||
if (value instanceof THREE.Texture) {
|
||||
value.dispose();
|
||||
}
|
||||
}
|
||||
material.dispose();
|
||||
}
|
||||
|
||||
static mesh(mesh: THREE.Mesh): void {
|
||||
mesh.geometry?.dispose();
|
||||
|
||||
const materials = Array.isArray(mesh.material)
|
||||
? mesh.material
|
||||
: [mesh.material];
|
||||
|
||||
for (const material of materials) {
|
||||
if (material) this.material(material);
|
||||
}
|
||||
}
|
||||
|
||||
static renderTarget(rt: THREE.WebGLRenderTarget): void {
|
||||
rt.texture.dispose();
|
||||
rt.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example usage:
|
||||
|
||||
```ts
|
||||
useEffect(() => {
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
});
|
||||
|
||||
meshRef.current.material = material;
|
||||
|
||||
return () => {
|
||||
Dispose.material(material);
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Rule:** disposal is ownership-based, not automatic and not blind.
|
||||
|
||||
---
|
||||
|
||||
## 6. Debug Utility
|
||||
|
||||
The debug panel can be activated by appending `?debug` to the URL:
|
||||
|
||||
`http://localhost:5173?debug`
|
||||
|
||||
All debug logic is centralized in `Debug.ts`.
|
||||
Do not scatter debug checks across the codebase.
|
||||
|
||||
```ts
|
||||
// utils/Debug.ts
|
||||
import GUI from "lil-gui";
|
||||
|
||||
export class Debug {
|
||||
private static _instance: Debug | null = null;
|
||||
|
||||
readonly active: boolean;
|
||||
gui: GUI | null = null;
|
||||
|
||||
static getInstance(): Debug {
|
||||
if (!Debug._instance) Debug._instance = new Debug();
|
||||
return Debug._instance;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
this.active = new URLSearchParams(window.location.search).has("debug");
|
||||
if (this.active) {
|
||||
this.gui = new GUI({ title: "La-Fabrik Debug" });
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.gui?.destroy();
|
||||
Debug._instance = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```ts
|
||||
const debug = Debug.getInstance();
|
||||
|
||||
if (debug.active) {
|
||||
debug.gui!.add(params, "bloomIntensity", 0, 3).name("Bloom");
|
||||
}
|
||||
```
|
||||
- The repository is still a prototype, not the full intended game runtime.
|
||||
- `src/world/debug/TestScene.tsx` is still part of the active scene composition.
|
||||
- There is no central gameplay orchestrator such as `GameManager` yet.
|
||||
- Missions, zones, cinematics, and dialogue systems are not implemented.
|
||||
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
|
||||
|
||||
@@ -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/rapier": "^2.2.0",
|
||||
"gsap": "^3.15.0",
|
||||
"lil-gui": "^0.21.0",
|
||||
"r3f-perf": "^7.2.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -30,7 +31,6 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"lil-gui": "^0.21.0",
|
||||
"prettier": "^3.8.2",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
@@ -293,9 +293,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -305,9 +305,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -484,29 +484,43 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/types": "^0.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/node": {
|
||||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
||||
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
|
||||
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/core": "^0.19.1",
|
||||
"@humanfs/core": "^0.19.2",
|
||||
"@humanfs/types": "^0.15.0",
|
||||
"@humanwhocodes/retry": "^0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
@@ -604,9 +618,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
|
||||
"integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -623,9 +637,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.124.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
|
||||
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
|
||||
"version": "0.127.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
|
||||
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -796,9 +810,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -813,9 +827,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -830,9 +844,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -847,9 +861,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -864,9 +878,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -881,9 +895,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -901,9 +915,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -921,9 +935,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -941,9 +955,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -961,9 +975,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -981,9 +995,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1001,9 +1015,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1018,9 +1032,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
@@ -1028,18 +1042,18 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.9.2",
|
||||
"@emnapi/runtime": "1.9.2",
|
||||
"@napi-rs/wasm-runtime": "^1.1.3"
|
||||
"@emnapi/core": "1.10.0",
|
||||
"@emnapi/runtime": "1.10.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1054,9 +1068,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1174,18 +1188,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.183.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
|
||||
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
|
||||
"version": "0.184.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.0.tgz",
|
||||
"integrity": "sha512-4mY2tZAu0y0B0567w7013BBXSpsP0+Z48NJvmNo4Y/Pf76yCyz6Jw4P3tUVs10WuYNXXZ+wmHyGWpCek3amJxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": ">=0.5.17",
|
||||
"@webgpu/types": "*",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~1.0.1"
|
||||
"meshoptimizer": "~1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/three/node_modules/@dimforge/rapier3d-compat": {
|
||||
@@ -1201,17 +1214,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz",
|
||||
"integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==",
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz",
|
||||
"integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.58.2",
|
||||
"@typescript-eslint/type-utils": "8.58.2",
|
||||
"@typescript-eslint/utils": "8.58.2",
|
||||
"@typescript-eslint/visitor-keys": "8.58.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.0",
|
||||
"@typescript-eslint/type-utils": "8.59.0",
|
||||
"@typescript-eslint/utils": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -1224,7 +1237,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.58.2",
|
||||
"@typescript-eslint/parser": "^8.59.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
@@ -1240,16 +1253,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz",
|
||||
"integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz",
|
||||
"integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.58.2",
|
||||
"@typescript-eslint/types": "8.58.2",
|
||||
"@typescript-eslint/typescript-estree": "8.58.2",
|
||||
"@typescript-eslint/visitor-keys": "8.58.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1265,14 +1278,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz",
|
||||
"integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==",
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz",
|
||||
"integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.58.2",
|
||||
"@typescript-eslint/types": "^8.58.2",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.0",
|
||||
"@typescript-eslint/types": "^8.59.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1287,14 +1300,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz",
|
||||
"integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==",
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz",
|
||||
"integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.58.2",
|
||||
"@typescript-eslint/visitor-keys": "8.58.2"
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1305,9 +1318,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz",
|
||||
"integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==",
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz",
|
||||
"integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1322,15 +1335,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz",
|
||||
"integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==",
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz",
|
||||
"integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.58.2",
|
||||
"@typescript-eslint/typescript-estree": "8.58.2",
|
||||
"@typescript-eslint/utils": "8.58.2",
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||
"@typescript-eslint/utils": "8.59.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -1347,9 +1360,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz",
|
||||
"integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==",
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz",
|
||||
"integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1361,16 +1374,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz",
|
||||
"integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==",
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz",
|
||||
"integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.58.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.58.2",
|
||||
"@typescript-eslint/types": "8.58.2",
|
||||
"@typescript-eslint/visitor-keys": "8.58.2",
|
||||
"@typescript-eslint/project-service": "8.59.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -1441,16 +1454,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz",
|
||||
"integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==",
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz",
|
||||
"integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.58.2",
|
||||
"@typescript-eslint/types": "8.58.2",
|
||||
"@typescript-eslint/typescript-estree": "8.58.2"
|
||||
"@typescript-eslint/scope-manager": "8.59.0",
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"@typescript-eslint/typescript-estree": "8.59.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1465,13 +1478,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz",
|
||||
"integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==",
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz",
|
||||
"integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.58.2",
|
||||
"@typescript-eslint/types": "8.59.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"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": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
@@ -1586,9 +1593,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1653,9 +1660,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.18",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz",
|
||||
"integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==",
|
||||
"version": "2.10.23",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz",
|
||||
"integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -1767,9 +1774,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001788",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
|
||||
"integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
|
||||
"version": "1.0.30001791",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
|
||||
"integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1927,9 +1934,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.336",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz",
|
||||
"integrity": "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==",
|
||||
"version": "1.5.344",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
|
||||
"integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -2064,9 +2071,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react-hooks": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
|
||||
"integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
|
||||
"integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2080,7 +2087,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"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": {
|
||||
@@ -2878,7 +2885,6 @@
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.21.0.tgz",
|
||||
"integrity": "sha512-tpvxN7v1GvE/Tv+GRopfOp0W7fVEjF4PltkuX8vOCIfim22rD1ztvfkoEMcv9lzQeuNUSeIrUmUjBwmlW/oUew==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
@@ -2946,9 +2952,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
||||
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
|
||||
"integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
@@ -3008,9 +3014,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.37",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
|
||||
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
|
||||
"version": "2.0.38",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
|
||||
"integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3126,9 +3132,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.9",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
|
||||
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
|
||||
"version": "8.5.12",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
|
||||
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3155,12 +3161,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postprocessing": {
|
||||
"version": "6.39.0",
|
||||
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.0.tgz",
|
||||
"integrity": "sha512-/G6JY8hs426lcto/pBZlnFSkyEo1fHsh4gy7FPJtq1SaSUOzJgDW6f6f1K/+aMOYzK/eQEefyOb3++jPPIUeDA==",
|
||||
"version": "6.39.1",
|
||||
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.1.tgz",
|
||||
"integrity": "sha512-R2dG2zy+BAx3USl5EHw+PvnrlbT5PKnZVp3se0HCR0pWH8WQdh742yNG4YWOsq6c0bFpffk0Gd2RqPeoP/wKng==",
|
||||
"license": "Zlib",
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.168.0 < 0.184.0"
|
||||
"three": ">= 0.168.0 < 0.185.0"
|
||||
}
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
@@ -3180,9 +3186,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz",
|
||||
"integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
||||
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -3521,14 +3527,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.124.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.15"
|
||||
"@oxc-project/types": "=0.127.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.17"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
@@ -3537,27 +3543,27 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3842,9 +3848,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
|
||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -3856,16 +3862,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.58.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz",
|
||||
"integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==",
|
||||
"version": "8.59.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz",
|
||||
"integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.58.2",
|
||||
"@typescript-eslint/parser": "8.58.2",
|
||||
"@typescript-eslint/typescript-estree": "8.58.2",
|
||||
"@typescript-eslint/utils": "8.58.2"
|
||||
"@typescript-eslint/eslint-plugin": "8.59.0",
|
||||
"@typescript-eslint/parser": "8.59.0",
|
||||
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||
"@typescript-eslint/utils": "8.59.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -3946,17 +3952,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
|
||||
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
||||
"version": "8.0.10",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
||||
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.8",
|
||||
"rolldown": "1.0.0-rc.15",
|
||||
"tinyglobby": "^0.2.15"
|
||||
"postcss": "^8.5.10",
|
||||
"rolldown": "1.0.0-rc.17",
|
||||
"tinyglobby": "^0.2.16"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
|
||||
+1
-1
@@ -19,6 +19,7 @@
|
||||
"@react-three/postprocessing": "^3.0.4",
|
||||
"@react-three/rapier": "^2.2.0",
|
||||
"gsap": "^3.15.0",
|
||||
"lil-gui": "^0.21.0",
|
||||
"r3f-perf": "^7.2.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -36,7 +37,6 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"lil-gui": "^0.21.0",
|
||||
"prettier": "^3.8.2",
|
||||
"typescript": "~6.0.2",
|
||||
"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 reactLogo from "./assets/react.svg";
|
||||
import viteLogo from "./assets/vite.svg";
|
||||
import heroImg from "./assets/hero.png";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
import { Suspense } from "react";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { Crosshair } from "@/components/ui/Crosshair";
|
||||
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
||||
import { DebugPerf } from "@/utils/debug/DebugPerf";
|
||||
import { World } from "@/world/World";
|
||||
|
||||
function App(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<section id="center">
|
||||
<div className="hero">
|
||||
<img src={heroImg} className="base" width="170" height="179" alt="" />
|
||||
<img src={reactLogo} className="framework" alt="React logo" />
|
||||
<img src={viteLogo} className="vite" alt="Vite logo" />
|
||||
</div>
|
||||
<div>
|
||||
<h1>Get started</h1>
|
||||
<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>
|
||||
<Canvas camera={{ position: [85, 60, 85], fov: 42 }} shadows>
|
||||
<Suspense fallback={null}>
|
||||
<World />
|
||||
<DebugPerf />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
<Crosshair />
|
||||
<InteractPrompt />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--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);
|
||||
}
|
||||
color-scheme: dark;
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #04070d;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 56px;
|
||||
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;
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
code,
|
||||
.counter {
|
||||
font-family: var(--mono);
|
||||
.crosshair {
|
||||
position: fixed;
|
||||
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;
|
||||
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;
|
||||
color: var(--text-h);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 15px;
|
||||
line-height: 135%;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
.interact-prompt__label {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<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