fix: archi

This commit is contained in:
2026-04-27 10:53:50 +02:00
parent 8c84663472
commit 393b653cca
16 changed files with 242 additions and 192 deletions
+39 -74
View File
@@ -1,90 +1,55 @@
# Agent La Fabrik # 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. You are working on **La Fabrik**, an interactive 3D web experience built with React Three Fiber.
## Project Identity ## Read This First
- **Stack:** React 19, Three.js, @react-three/fiber 9, @react-three/drei, @react-three/rapier, GSAP, TypeScript, Vite - `docs/technical/architecture.md` describes the code that exists today.
- **No external state lib.** State is managed by a custom `GameManager` singleton with a subscribe/getState pattern. - `docs/technical/target-architecture.md` describes the intended target-state.
- **No Zustand, no Redux, no Context for global state.** - Do not assume target-state systems already exist.
- **Versions are pinned** (no `^` in dependencies). Do not upgrade packages without explicit request.
## Architecture Rules ## Current Implementation
### Two patterns coexist - 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
1. **Singleton manager classes** — for orchestration, audio, cinematics, zone detection, debug ## Current Architecture Rules
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. - 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.
### State ownership ## Target-State Guidance
- `GameManager` is the single source of truth for durable gameplay state (phase, zone, mission, input lock, dialogue) The project may later grow toward a manager-driven gameplay architecture with clearer separation between:
- 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 - production world code
- gameplay orchestration
- UI overlays
- debug tooling
- Every file starts with a comment: `# route path <relative_path>` (e.g. `# route path src/world/Map.tsx`) 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.
- Scene components live in `src/world/` and `src/components/3d/`
- UI overlays live in `src/components/ui/`
- Managers live in `src/stateManager/`
- Debug tooling lives in `src/utils/debug/`
- Hooks live in `src/hooks/`
- Static data lives in `src/data/`
- Shaders live in `src/shaders/`
- Utilities live in `src/utils/`
### Import paths ## Do Not Assume
Use `@/` alias for imports from `src/`: - There is no `GameManager` in the current codebase.
- There are no implemented mission, zone, cinematic, or dialogue systems yet.
```ts - Dependency versions are not pinned today; do not rewrite dependency strategy unless explicitly asked.
import { GameManager } from "@/stateManager/GameManager"; - The old `# route path ...` file header convention is not in use.
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/Debug.ts`
- Never scatter `if (isDev)` blocks across files
- `r3f-perf` is lazy-loaded only in debug mode via `src/utils/debug/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 ## Skills
See `.agent/skills/` for detailed patterns per technology: 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.
- `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
+32 -28
View File
@@ -1,43 +1,47 @@
# Implemented Architecture # Current Architecture
This document describes the code that exists today in the repository. This document describes the code that exists today in the repository.
## Runtime Structure ## Runtime Structure
- `src/App.tsx` mounts the `Canvas`, the 3D `World`, the debug perf overlay, and the HTML crosshair overlay. - `src/App.tsx` mounts the `Canvas`, the 3D `World`, the debug perf overlay, and the HTML overlays.
- `src/world/World.tsx` composes the active 3D scene. - `src/world/World.tsx` composes the active scene, including:
- `src/world/Map.tsx` loads and centers the blocking map model. - environment and lighting
- `src/world/Lighting.tsx` owns the current ambient and directional light setup. - debug helpers and debug camera mode
- `src/world/Environment.tsx` owns the current background color. - either the map scene or the debug physics test scene
- `src/world/player/FPSController.tsx` provides the current player camera, pointer lock, and `ZQSD` movement. - the player rig when the active camera mode is `player`
- `src/utils/debug/` contains debug-only tooling such as `lil-gui`, scene helpers, and the free debug camera. - `src/world/Map.tsx` loads the main map model and builds the collision octree.
- `src/components/ui/Crosshair.tsx` is the only current HTML overlay component in use. - `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.
## Camera Modes ## Interaction Model
The application currently has two camera modes: - `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.
- `player` ## Audio
- controlled by `FPSController`
- player height is `1.75m`
- movement uses `ZQSD`
- `E` is reserved for future interaction
- `debug`
- controlled by `DebugCameraControls`
- enabled from the debug panel
The active mode is stored in the debug subsystem and consumed through `src/hooks/debug/useCameraMode.ts`. - `src/stateManager/AudioManager.ts` currently provides pooled one-shot sound playback.
- Trigger interactions may play audio directly through `AudioManager`.
## Debug System ## Debug System
- `src/utils/debug/Debug.ts` is a singleton wrapper around `lil-gui` - Debug mode is enabled with `?debug`.
- `src/utils/debug/DebugPerf.tsx` lazy-loads `r3f-perf` - `src/utils/debug/Debug.ts` owns the `lil-gui` instance and debug controls.
- `src/utils/debug/scene/DebugHelpers.tsx` mounts grid and axes in debug mode - `src/hooks/debug/useCameraMode.ts` and `src/hooks/debug/useSceneMode.ts` subscribe to debug state.
- `src/utils/debug/scene/DebugCameraControls.tsx` mounts the free camera in debug mode - `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.
## Current Limitations ## Current Limitations
- There is no gameplay state manager implemented yet. - The repository is still a prototype, not the full intended game runtime.
- There are no zone systems, missions, dialogue systems, or cinematic systems implemented yet. - `src/world/debug/TestScene.tsx` is still part of the active scene composition.
- Player movement currently uses a simple height clamp instead of real collision or ground detection. - There is no central gameplay orchestrator such as `GameManager` yet.
- The map is currently a blocking preview scene, not a full playable world. - Missions, zones, cinematics, and dialogue systems are not implemented.
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
+27 -18
View File
@@ -2,60 +2,69 @@
This document describes the intended medium-term architecture for the project. 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 ## Goals
- Keep `main` stable, `develop` as the integration branch, and `feat/*` for feature work. - Keep `App.tsx` small and orchestration-oriented.
- Keep the runtime split between scene composition, gameplay systems, debug tooling, and HTML UI. - Separate production world code from debug-only runtime paths.
- Keep one clear source of truth per concern. - Keep one clear source of truth per concern.
- Grow gameplay systems incrementally instead of prebuilding empty architecture.
## Intended Layers ## Intended Layers
### App Layer ### App Layer
- `App.tsx` should stay small and orchestration-oriented. - `App.tsx` mounts the canvas scene and top-level HTML overlays.
- It should mount the canvas scene and top-level HTML overlays. - It should stay thin and avoid gameplay logic.
### World Layer ### World Layer
- `src/world/` should contain only production scene objects and scene composition. - `src/world/` should contain production scene composition and production scene objects.
- Expected responsibilities: - Expected responsibilities:
- world composition - world composition
- map/environment/lighting - map, environment, lighting
- player controller - player controller
- zones - production interaction anchors
- post-processing used in production - production post-processing, if needed
### Debug Layer ### Debug Layer
- `src/utils/debug/` should contain only developer tooling. - Debug-only scenes and tooling should be isolated from the production world path.
- Expected responsibilities: - Expected responsibilities:
- `lil-gui` - `lil-gui`
- performance overlay - performance overlay
- scene helpers - scene helpers
- free camera and calibration controls - free camera and calibration controls
- temporary test scenes used during development
### UI Layer ### UI Layer
- `src/components/ui/` should contain HTML overlays used by the player. - `src/components/ui/` should contain player-facing HTML overlays.
- Expected examples: - Expected future examples:
- crosshair - crosshair
- loading screen - loading flow
- mission HUD - mission HUD
- narrative overlays - narrative overlays
### Gameplay Layer ### Gameplay Layer
- Gameplay state should eventually live in dedicated managers and thin hooks once those systems exist. - As the project grows, gameplay state can move toward a clearer orchestration layer.
- Expected future concerns: - Likely future concerns:
- missions - missions
- zones - zones
- cinematics - cinematics
- dialogue
- audio - audio
- interactions - interactions
## Rules ## Rules
- `world/` should not contain debug-only tooling. - Prefer direct, working code over speculative scaffolding.
- `debug/` should not own production gameplay systems. - Shared types should stay close to their domain until they have multiple real consumers.
- Shared types should live close to their domain and move outward only when they gain multiple real consumers. - Avoid creating new managers or service layers without an active runtime need.
- New files should only be created when they have an active runtime purpose. - Debug-only runtime paths should be clearly marked and easy to remove later.
+28 -23
View File
@@ -1,44 +1,49 @@
# Implemented Features # Implemented Features
This document lists features that are actually implemented in the current codebase. This document lists features that are implemented in the current codebase.
## Scene Preview ## Scene
- Fullscreen React Three Fiber scene - Fullscreen React Three Fiber scene
- Blocking map loaded from `public/models/map/blocking/model.glb` - Main map scene loaded from `public/models/map/model.gltf`
- Debug physics test scene selectable from the debug panel
- Ambient and directional lighting - Ambient and directional lighting
- Solid background environment color - Environment background setup
## Camera Modes ## Player
- Player camera mode - Player camera mode
- eye height at `1.75m` - Pointer lock mouse look
- pointer lock mouse look - Movement with `ZQSD`
- movement with `ZQSD` - Jumping
- vertical clamp to prevent falling below the map plane - Octree-based collision against the loaded map
- Debug camera mode
- free orbit camera
- switchable from the debug panel
## UI ## Interactions
- Center-screen crosshair shown only in player mode - 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 Tooling
- `?debug` query param enables the debug panel - `?debug` query param enables the debug panel
- `lil-gui` panel with camera mode selection - `lil-gui` controls for camera mode, scene mode, and interaction spheres
- debug lighting controls - Debug scene helpers
- debug scene helpers - Free debug camera
- `r3f-perf` overlay - `r3f-perf` overlay
## Not Implemented Yet ## Not Implemented Yet
- missions - mission system
- interactions on `E` - zone system
- gameplay zones - cinematic system
- cinematics - dialogue system
- audio systems
- loading flow - loading flow
- minimap and mission HUD - minimap and mission HUD
- collisions beyond the current simple player height clamp - full production separation between gameplay and debug scenes
+55 -25
View File
@@ -1,7 +1,8 @@
import { useEffect, useRef } from "react"; import { useCallback, useEffect, useRef } from "react";
import { useFrame, useThree } from "@react-three/fiber"; import { useFrame, useThree } from "@react-three/fiber";
import type { RapierRigidBody } from "@react-three/rapier"; import type { RapierRigidBody } from "@react-three/rapier";
import * as THREE from "three"; import * as THREE from "three";
import type GUI from "lil-gui";
import type { RefObject } from "react"; import type { RefObject } from "react";
import { import {
INTERACTION_DEBUG_SPHERE_COLOR, INTERACTION_DEBUG_SPHERE_COLOR,
@@ -13,54 +14,83 @@ import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
import { InteractionManager } from "@/stateManager/InteractionManager"; import { InteractionManager } from "@/stateManager/InteractionManager";
import { INTERACTION_RADIUS } from "@/data/interactionConfig"; import { INTERACTION_RADIUS } from "@/data/interactionConfig";
import type { Vector3Tuple } from "@/types/3d"; import type { Vector3Tuple } from "@/types/3d";
import type { InteractableHandle, InteractableKind } from "@/types/interaction"; import type {
GrabInteractableHandle,
InteractableHandle,
TriggerInteractableHandle,
} from "@/types/interaction";
interface InteractableObjectProps { interface InteractableObjectBaseProps {
kind: InteractableKind;
label: string; label: string;
position: Vector3Tuple; position: Vector3Tuple;
bodyRef?: RefObject<RapierRigidBody | null>; bodyRef?: RefObject<RapierRigidBody | null>;
onPress: () => void; onPress: () => void;
onRelease?: () => void;
children: React.ReactNode; children: React.ReactNode;
} }
interface TriggerInteractableObjectProps extends InteractableObjectBaseProps {
kind: "trigger";
}
interface GrabInteractableObjectProps extends InteractableObjectBaseProps {
kind: "grab";
onRelease: () => void;
}
type InteractableObjectProps =
| TriggerInteractableObjectProps
| GrabInteractableObjectProps;
const _cameraPos = new THREE.Vector3(); const _cameraPos = new THREE.Vector3();
const _cameraDir = new THREE.Vector3(); const _cameraDir = new THREE.Vector3();
const _objectPos = new THREE.Vector3(); const _objectPos = new THREE.Vector3();
const _raycaster = new THREE.Raycaster(); const _raycaster = new THREE.Raycaster();
export function InteractableObject({ export function InteractableObject(
kind, props: InteractableObjectProps,
label, ): React.JSX.Element {
position, const { kind, label, position, bodyRef, onPress, children } = props;
bodyRef,
onPress,
onRelease = () => {},
children,
}: InteractableObjectProps): React.JSX.Element {
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
const debugSphereRef = useRef<THREE.Mesh>(null); const debugSphereRef = useRef<THREE.Mesh>(null);
const handle = useRef<InteractableHandle>({ const handle = useRef<InteractableHandle>(
kind, props.kind === "grab"
label, ? { kind: props.kind, label, onPress, onRelease: props.onRelease }
onPress, : { kind: props.kind, label, onPress },
onRelease, );
});
useEffect(() => { useEffect(() => {
handle.current.onPress = onPress; if (props.kind === "grab") {
handle.current.onRelease = onRelease; const current = handle.current as GrabInteractableHandle;
}); current.label = label;
current.onPress = onPress;
current.onRelease = props.onRelease;
return;
}
useDebugFolder("Interaction", (folder) => { return undefined;
}, [label, onPress, props]);
useEffect(() => {
if (kind === "grab") {
return undefined;
}
const current = handle.current as TriggerInteractableHandle;
current.label = label;
current.onPress = onPress;
return undefined;
}, [kind, label, onPress]);
const setupInteractionDebugFolder = useCallback((folder: GUI) => {
folder folder
.add({ radius: INTERACTION_RADIUS }, "radius") .add({ radius: INTERACTION_RADIUS }, "radius")
.name("Interaction radius") .name("Interaction radius")
.disable(); .disable();
}); }, []);
useDebugFolder("Interaction", setupInteractionDebugFolder);
useFrame(() => { useFrame(() => {
const group = groupRef.current; const group = groupRef.current;
+11 -2
View File
@@ -35,7 +35,7 @@ export class AudioManager {
logger.error("AudioManager", "Failed to play sound", { logger.error("AudioManager", "Failed to play sound", {
path, path,
error, error: AudioManager._toLogValue(error),
}); });
}); });
} }
@@ -66,11 +66,20 @@ export class AudioManager {
return pooledAudio; return pooledAudio;
} }
return existingPool[0]!; const recycledAudio = existingPool[0];
if (recycledAudio) return recycledAudio;
} }
const initialAudio = new Audio(path); const initialAudio = new Audio(path);
this._audioPools.set(path, [initialAudio]); this._audioPools.set(path, [initialAudio]);
return initialAudio; return initialAudio;
} }
private static _toLogValue(error: unknown): Error | DOMException | string {
if (error instanceof Error || error instanceof DOMException) {
return error;
}
return String(error);
}
} }
+10 -3
View File
@@ -1,4 +1,5 @@
import type { import type {
GrabInteractableHandle,
InteractableHandle, InteractableHandle,
InteractionSnapshot, InteractionSnapshot,
} from "@/types/interaction"; } from "@/types/interaction";
@@ -8,7 +9,7 @@ export class InteractionManager {
private _focused: InteractableHandle | null = null; private _focused: InteractableHandle | null = null;
private _holding = false; private _holding = false;
private _holdingHandle: InteractableHandle | null = null; private _holdingHandle: GrabInteractableHandle | null = null;
private _snapshot: InteractionSnapshot = { private _snapshot: InteractionSnapshot = {
focused: null, focused: null,
holding: false, holding: false,
@@ -40,8 +41,14 @@ export class InteractionManager {
pressInteract(): void { pressInteract(): void {
if (!this._focused) return; if (!this._focused) return;
this._holding = this._focused.kind === "grab"; if (this._focused.kind === "grab") {
if (this._holding) this._holdingHandle = this._focused; this._holding = true;
this._holdingHandle = this._focused;
} else {
this._holding = false;
this._holdingHandle = null;
}
this._focused.onPress(); this._focused.onPress();
this._emit(); this._emit();
} }
+12 -2
View File
@@ -1,12 +1,22 @@
export type InteractableKind = "grab" | "trigger"; export type InteractableKind = "grab" | "trigger";
export interface InteractableHandle { export interface TriggerInteractableHandle {
kind: InteractableKind; kind: "trigger";
label: string;
onPress: () => void;
}
export interface GrabInteractableHandle {
kind: "grab";
label: string; label: string;
onPress: () => void; onPress: () => void;
onRelease: () => void; onRelease: () => void;
} }
export type InteractableHandle =
| TriggerInteractableHandle
| GrabInteractableHandle;
export interface InteractionSnapshot { export interface InteractionSnapshot {
focused: InteractableHandle | null; focused: InteractableHandle | null;
holding: boolean; holding: boolean;
+12 -1
View File
@@ -1,6 +1,17 @@
export type LogLevel = "debug" | "info" | "warn" | "error"; export type LogLevel = "debug" | "info" | "warn" | "error";
export type LogContext = Record<string, unknown>; export type LogValue =
| string
| number
| boolean
| null
| undefined
| Error
| DOMException
| { [key: string]: LogValue }
| LogValue[];
export type LogContext = Readonly<Record<string, LogValue>>;
export interface LogEntry { export interface LogEntry {
timestamp: string; timestamp: string;
+2 -1
View File
@@ -1,5 +1,6 @@
import GUI from "lil-gui"; import GUI from "lil-gui";
import type { CameraMode, SceneMode } from "@/types/debug"; import type { CameraMode, SceneMode } from "@/types/debug";
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
export class Debug { export class Debug {
private static instance: Debug | null = null; private static instance: Debug | null = null;
@@ -28,7 +29,7 @@ export class Debug {
} }
private constructor() { private constructor() {
this.active = new URLSearchParams(window.location.search).has("debug"); this.active = isDebugEnabled();
this.gui = this.active ? new GUI({ title: "La-Fabrik Debug" }) : null; this.gui = this.active ? new GUI({ title: "La-Fabrik Debug" }) : null;
if (this.gui) { if (this.gui) {
+7
View File
@@ -0,0 +1,7 @@
export function isDebugEnabled(): boolean {
if (typeof window === "undefined") {
return false;
}
return new URLSearchParams(window.location.search).has("debug");
}
+2 -3
View File
@@ -4,6 +4,7 @@ import type {
LogLevel, LogLevel,
LoggerConfig, LoggerConfig,
} from "@/types/logger"; } from "@/types/logger";
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
const LEVEL_PRIORITY: Record<LogLevel, number> = { const LEVEL_PRIORITY: Record<LogLevel, number> = {
debug: 10, debug: 10,
@@ -102,9 +103,7 @@ function resolveMinLevel(): LogLevel {
return "info"; return "info";
} }
const debugEnabled = new URLSearchParams(window.location.search).has("debug"); return isDebugEnabled() ? "debug" : "info";
return debugEnabled ? "debug" : "info";
} }
export const logger = new Logger({ export const logger = new Logger({
+3 -4
View File
@@ -1,4 +1,4 @@
import { useState, useCallback } from "react"; import { useState } from "react";
import type { Octree } from "three/addons/math/Octree.js"; import type { Octree } from "three/addons/math/Octree.js";
import { import {
PLAYER_SPAWN_Y_GAME, PLAYER_SPAWN_Y_GAME,
@@ -18,7 +18,6 @@ export function World(): React.JSX.Element {
const cameraMode = useCameraMode(); const cameraMode = useCameraMode();
const sceneMode = useSceneMode(); const sceneMode = useSceneMode();
const [octree, setOctree] = useState<Octree | null>(null); const [octree, setOctree] = useState<Octree | null>(null);
const onOctreeReady = useCallback((o: Octree) => setOctree(o), []);
return ( return (
<> <>
@@ -28,9 +27,9 @@ export function World(): React.JSX.Element {
{cameraMode === "debug" ? <DebugCameraControls /> : null} {cameraMode === "debug" ? <DebugCameraControls /> : null}
{sceneMode === "game" ? ( {sceneMode === "game" ? (
<Map onOctreeReady={onOctreeReady} /> <Map onOctreeReady={setOctree} />
) : ( ) : (
<TestScene onOctreeReady={onOctreeReady} /> <TestScene onOctreeReady={setOctree} />
)} )}
{cameraMode !== "debug" ? ( {cameraMode !== "debug" ? (
+2 -2
View File
@@ -6,13 +6,13 @@ import { PlayerCamera } from "@/world/player/PlayerCamera";
import { PlayerController } from "@/world/player/PlayerController"; import { PlayerController } from "@/world/player/PlayerController";
interface PlayerComponentProps { interface PlayerComponentProps {
octree?: Octree | null; octree: Octree | null;
spawnY: number; spawnY: number;
} }
export function PlayerComponent({ export function PlayerComponent({
octree = null,
spawnY, spawnY,
octree,
}: PlayerComponentProps): React.JSX.Element { }: PlayerComponentProps): React.JSX.Element {
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
-5
View File
@@ -123,11 +123,6 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
case MOVE_RIGHT_KEY: case MOVE_RIGHT_KEY:
keys.current.right = false; keys.current.right = false;
break; break;
case INTERACT_KEY:
if (interaction.getState().focused?.kind === "trigger") {
interaction.releaseInteract();
}
break;
default: default:
return; return;
} }
-1
View File
@@ -2,7 +2,6 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import path from "node:path"; import path from "node:path";
// https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: { resolve: {