diff --git a/.agent/AGENT.md b/.agent/AGENT.md new file mode 100644 index 0000000..0f732ba --- /dev/null +++ b/.agent/AGENT.md @@ -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. diff --git a/.agent/AGENTS.md b/.agent/AGENTS.md deleted file mode 100644 index 425e13e..0000000 --- a/.agent/AGENTS.md +++ /dev/null @@ -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 ` (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 diff --git a/.agent/skills/debug.md b/.agent/skills/debug.md index b0e14a3..94ae992 100644 --- a/.agent/skills/debug.md +++ b/.agent/skills/debug.md @@ -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 ( diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cc775b3..265b858 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c72f91c..c787ccb 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -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 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5b924de --- /dev/null +++ b/.prettierignore @@ -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 diff --git a/README.md b/README.md index 8465be0..b7acd20 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md index bb03d9f..769575d 100644 --- a/docs/technical/architecture.md +++ b/docs/technical/architecture.md @@ -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 ``. -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(null); - const gltf = useGLTF("/models/workshop/ebike.glb"); - const mixer = useRef(null); - - useEffect(() => { - mixer.current = new THREE.AnimationMixer(gltf.scene); - - return () => { - mixer.current?.stopAllAction(); - mixer.current = null; - }; - }, [gltf.scene]); - - useFrame((_, delta) => { - mixer.current?.update(delta); - }); - - return ; -} -``` - -Per-frame values such as movement, interpolation, camera smoothing, and physics must stay in: - -- `useRef` -- `useFrame` -- Rapier bodies -- other frame-based systems - -They must **never** go through React state. - ---- - -## 3. Single Source of Truth for Durable Gameplay State - -The project uses a single authoritative `GameManager` for durable gameplay state. - -React components subscribe to that state through thin hooks. -Other managers communicate through `GameManager`, which acts as the main gameplay orchestrator. - -High-frequency values such as movement, camera interpolation, or physics never go through React state and stay in refs or frame-based systems. - -```ts -// stateManager/GameManager.ts -type Phase = "loading" | "intro" | "exploring" | "cinematic" | "outro"; -type ZoneId = "workshop" | "powerGrid" | "farm" | null; - -type GameSnapshot = { - phase: Phase; - activeZone: ZoneId; - missionId: string | null; - missionStep: number; - inputLocked: boolean; - dialogueId: string | null; -}; - -export class GameManager { - private static _instance: GameManager | null = null; - private listeners = new Set<() => void>(); - - private state: GameSnapshot = { - phase: "loading", - activeZone: null, - missionId: null, - missionStep: 0, - inputLocked: false, - dialogueId: null, - }; - - static getInstance(): GameManager { - if (!GameManager._instance) { - GameManager._instance = new GameManager(); - } - return GameManager._instance; - } - - getState(): GameSnapshot { - return this.state; - } - - subscribe(listener: () => void): () => void { - this.listeners.add(listener); - return () => this.listeners.delete(listener); - } - - private emit(): void { - this.listeners.forEach((cb) => cb()); - } - - setPhase(phase: Phase): void { - this.state.phase = phase; - this.emit(); - } - - setActiveZone(zone: ZoneId): void { - this.state.activeZone = zone; - this.emit(); - } - - startMission(id: string): void { - this.state.missionId = id; - this.state.missionStep = 0; - this.emit(); - } -} -``` - -```ts -// hooks/useGameState.ts -import { useEffect, useState } from "react"; -import { GameManager } from "@/stateManager/GameManager"; - -export function useGameState() { - const game = GameManager.getInstance(); - const [state, setState] = useState(game.getState()); - - useEffect(() => { - return game.subscribe(() => { - setState({ ...game.getState() }); - }); - }, [game]); - - return state; -} -``` - -This keeps the architecture simple: - -- **GameManager** owns durable gameplay state -- **other managers** handle side effects -- **React components** render that state -- **R3F frame systems** handle fast-changing values - ---- - -## 4. Side Effects Stay in Specialized Managers - -Managers other than `GameManager` should not become secondary state stores. - -Their role is to manage side effects and specialized runtime logic, such as: - -- GSAP timelines -- audio playback -- zone entry detection -- interaction triggers -- camera lock/unlock -- temporary event coordination - -They can read from `GameManager`, react to its state, or notify it of important transitions. - -Example flow: - -``` -Component / Hook - ↓ -GameManager.getInstance() - ├── startMission('workshop') - ├── cinematic.play('intro_workshop') - ├── audio.playAmbience('workshop') - └── zone.setActive('workshop') -``` - -This keeps the dependency graph understandable while avoiding duplicated durable state. - ---- - -## 5. Memory Management — Dispose Only What You Own - -GPU memory must be cleaned carefully. - -However, the project does **not** blindly deep-dispose every object on unmount. -Only resources explicitly created and owned by the current component or manager should be disposed. - -This includes things like: - -- custom materials -- render targets -- postprocessing passes -- manually created geometries -- manually created textures -- temporary clones with owned resources - -Shared or cached assets must **not** be blindly disposed. - -```ts -// utils/Dispose.ts -import * as THREE from "three"; - -export class Dispose { - static material(material: THREE.Material): void { - for (const value of Object.values(material)) { - if (value instanceof THREE.Texture) { - value.dispose(); - } - } - material.dispose(); - } - - static mesh(mesh: THREE.Mesh): void { - mesh.geometry?.dispose(); - - const materials = Array.isArray(mesh.material) - ? mesh.material - : [mesh.material]; - - for (const material of materials) { - if (material) this.material(material); - } - } - - static renderTarget(rt: THREE.WebGLRenderTarget): void { - rt.texture.dispose(); - rt.dispose(); - } -} -``` - -Example usage: - -```ts -useEffect(() => { - const material = new THREE.ShaderMaterial({ - vertexShader, - fragmentShader, - }); - - meshRef.current.material = material; - - return () => { - Dispose.material(material); - }; -}, []); -``` - -**Rule:** disposal is ownership-based, not automatic and not blind. - ---- - -## 6. Debug Utility - -The debug panel can be activated by appending `?debug` to the URL: - -`http://localhost:5173?debug` - -All debug logic is centralized in `Debug.ts`. -Do not scatter debug checks across the codebase. - -```ts -// utils/Debug.ts -import GUI from "lil-gui"; - -export class Debug { - private static _instance: Debug | null = null; - - readonly active: boolean; - gui: GUI | null = null; - - static getInstance(): Debug { - if (!Debug._instance) Debug._instance = new Debug(); - return Debug._instance; - } - - private constructor() { - this.active = new URLSearchParams(window.location.search).has("debug"); - if (this.active) { - this.gui = new GUI({ title: "La-Fabrik Debug" }); - } - } - - destroy(): void { - this.gui?.destroy(); - Debug._instance = null; - } -} -``` - -Usage: - -```ts -const debug = Debug.getInstance(); - -if (debug.active) { - debug.gui!.add(params, "bloomIntensity", 0, 3).name("Bloom"); -} -``` +- 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. diff --git a/docs/technical/best-practices.md b/docs/technical/best-practices.md deleted file mode 100644 index a70fb3e..0000000 --- a/docs/technical/best-practices.md +++ /dev/null @@ -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 ; -``` - -### Avoid Nested Callbacks - -```ts -// Good — flat structure -useEffect(() => { - const mixer = new THREE.AnimationMixer(model); - - return () => mixer.stopAllAction(); -}, [model]); -``` - -## TypeScript Rules - -### Explicit Types for Exports - -```ts -export function useGameState(): GameSnapshot { ... } - -export function setPhase(phase: Phase): void { ... } -``` - -### Never use `any` - -```ts -// Good -const ref = useRef(null); - -// Bad -const ref = useRef(null); -``` - -## Performance - -### useRef for Mutable Values - -```ts -// Good — no re-render -const position = useRef(new THREE.Vector3()); - -// Bad — triggers re-render every frame -const [position, setPosition] = useState(new THREE.Vector3()); -``` - -### Memoize Expensive Computations - -```ts -const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); -``` - -## Scalability - -### Single Responsibility - -```ts -// Good — focused component -export function WorkshopZone() { - // Only handles workshop zone logic -} - -// Bad — does everything -export function WorkshopZone() { - // Handles zone logic + audio + cinematics + missions -} -``` - -### Constants for Magic Numbers - -```ts -const DEFAULT_CAMERA_DISTANCE = 5; -const ZONE_DETECTION_RADIUS = 20; -``` - -## Accessibility (W3C) - -### Semantic HTML - -```tsx -// Good - - -// Bad -
- -
-``` - -## Rules - -1. **Simplicity** — Every line of code must be justified. -2. **Readability** — Code is read 10x more than it's written. -3. **Reviewability** — PRs should be understandable in < 5 minutes. -4. **Scalability** — Architecture should support growth without refactoring. -5. **Performance** — Don't optimize prematurely, but don't introduce obvious bottlenecks. -6. **Modern** — Use ES2022+ features, TypeScript strict mode, React hooks. -7. **W3C** — Follow web standards: semantic HTML, ARIA, keyboard navigation. -8. **No Over-Engineering** — Avoid patterns that add complexity without benefit. diff --git a/docs/technical/target-architecture.md b/docs/technical/target-architecture.md new file mode 100644 index 0000000..9ad9c0e --- /dev/null +++ b/docs/technical/target-architecture.md @@ -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. diff --git a/docs/user/features.md b/docs/user/features.md index ce78679..133c126 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 2a5d0f3..4fbc348 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 65ae60d..3a737f0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/models/environment/README.md b/public/models/environment/README.md deleted file mode 100644 index 4a1490d..0000000 --- a/public/models/environment/README.md +++ /dev/null @@ -1 +0,0 @@ -# public/models/environment/\* diff --git a/public/models/farm/README.md b/public/models/farm/README.md deleted file mode 100644 index 97a0558..0000000 --- a/public/models/farm/README.md +++ /dev/null @@ -1 +0,0 @@ -# public/models/farm/\* diff --git a/public/models/general/README.md b/public/models/general/README.md deleted file mode 100644 index a7b53a8..0000000 --- a/public/models/general/README.md +++ /dev/null @@ -1 +0,0 @@ -# public/models/general/\* diff --git a/public/models/map/blocking/model.gltf b/public/models/map/blocking/model.gltf deleted file mode 100644 index e13b317..0000000 --- a/public/models/map/blocking/model.gltf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c98eb2de597f5e0fd60e2caff3e1889c761bfdcb9d23488d31700cb850cdd2ea -size 2800791 diff --git a/public/models/map/model.gltf b/public/models/map/model.gltf new file mode 100644 index 0000000..f477d5c --- /dev/null +++ b/public/models/map/model.gltf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3535a67501bb43ccf233a25e98b20b3804e29f1fe7ef8ba821bbdd00b98f140 +size 3279070 diff --git a/public/models/powerGrid/README.md b/public/models/powerGrid/README.md deleted file mode 100644 index 0e17831..0000000 --- a/public/models/powerGrid/README.md +++ /dev/null @@ -1 +0,0 @@ -# public/models/powergrid/\* diff --git a/public/models/workshop/README.md b/public/models/workshop/README.md deleted file mode 100644 index 3eac41d..0000000 --- a/public/models/workshop/README.md +++ /dev/null @@ -1 +0,0 @@ -# public/models/workshop/\* diff --git a/public/skybox/sky.exr b/public/skybox/sky.exr new file mode 100644 index 0000000..c6ff8e1 --- /dev/null +++ b/public/skybox/sky.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:904b303c98f865526b9524b955f440e630f29d8e18a57bb7bf443fcd9715add1 +size 83079911 diff --git a/public/sounds/README.md b/public/sounds/README.md deleted file mode 100644 index 6526135..0000000 --- a/public/sounds/README.md +++ /dev/null @@ -1 +0,0 @@ -# public/sounds/\* diff --git a/public/sounds/fa.mp3 b/public/sounds/fa.mp3 new file mode 100644 index 0000000..c0a2b41 --- /dev/null +++ b/public/sounds/fa.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f714331f0f9ad760ae59d1c7cd4a6eb10b853c018f9764564e306b2f2444e56 +size 149972 diff --git a/public/textures/README.md b/public/textures/README.md deleted file mode 100644 index e8c8b42..0000000 --- a/public/textures/README.md +++ /dev/null @@ -1 +0,0 @@ -# public/textures/\* diff --git a/src/App.css b/src/App.css deleted file mode 100644 index f460279..0000000 --- a/src/App.css +++ /dev/null @@ -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); - } -} diff --git a/src/App.tsx b/src/App.tsx index 9331cb0..5879bcd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( <> -
-
- - React logo - Vite logo -
-
-

Get started

-

- Edit src/App.tsx and save to test HMR -

-
- -
- -
- -
-
- -

Documentation

-

Your questions, answered

- -
-
- -

Connect with us

-

Join the Vite community

- -
-
- -
-
+ + + + + + + + ); } diff --git a/src/assets/hero.png b/src/assets/hero.png deleted file mode 100644 index cc51a3d..0000000 Binary files a/src/assets/hero.png and /dev/null differ diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/src/components/3d/GrabbableObject.tsx b/src/components/3d/GrabbableObject.tsx new file mode 100644 index 0000000..0499484 --- /dev/null +++ b/src/components/3d/GrabbableObject.tsx @@ -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(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 ( + + { + 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} + + + ); +} diff --git a/src/components/3d/InteractableObject.tsx b/src/components/3d/InteractableObject.tsx new file mode 100644 index 0000000..4403579 --- /dev/null +++ b/src/components/3d/InteractableObject.tsx @@ -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; + 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(null); + const debugSphereRef = useRef(null); + + const handle = useRef( + 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 ( + + {children} + + + + + + ); +} diff --git a/src/components/3d/InteractiveObject.tsx b/src/components/3d/InteractiveObject.tsx deleted file mode 100644 index b07fb2f..0000000 --- a/src/components/3d/InteractiveObject.tsx +++ /dev/null @@ -1 +0,0 @@ -// src/components/3d/InteractiveObject.tsx diff --git a/src/components/3d/TriggerObject.tsx b/src/components/3d/TriggerObject.tsx new file mode 100644 index 0000000..937dfae --- /dev/null +++ b/src/components/3d/TriggerObject.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 ; +} + +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([]); + + return ( + <> + + { + 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} + + + + {spawnModel && + spawned.map((s) => ( + + ))} + + ); +} diff --git a/src/components/ui/CinematicBars.tsx b/src/components/ui/CinematicBars.tsx deleted file mode 100644 index 5c38ed7..0000000 --- a/src/components/ui/CinematicBars.tsx +++ /dev/null @@ -1 +0,0 @@ -// src/components/ui/CinematicBars.tsx diff --git a/src/components/ui/Crosshair.tsx b/src/components/ui/Crosshair.tsx new file mode 100644 index 0000000..dae485e --- /dev/null +++ b/src/components/ui/Crosshair.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 ( +