fix: archi
This commit is contained in:
+39
-74
@@ -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
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function isDebugEnabled(): boolean {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URLSearchParams(window.location.search).has("debug");
|
||||||
|
}
|
||||||
+2
-3
@@ -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
@@ -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" ? (
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user