refacto : cleaning the codebasebase again
This commit is contained in:
@@ -126,8 +126,8 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
- `http://localhost:5173` for the app
|
- app: `http://localhost:5173`
|
||||||
- `http://localhost:5173?debug` to enable debug tooling
|
- debug mode: `http://localhost:5173?debug`
|
||||||
|
|
||||||
## 📜 License
|
## 📜 License
|
||||||
|
|
||||||
|
|||||||
Generated
-5589
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
"madge": "^8.0.0",
|
|
||||||
"prettier": "^3.8.2",
|
"prettier": "^3.8.2",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.58.0",
|
"typescript-eslint": "^8.58.0",
|
||||||
|
|||||||
@@ -21,15 +21,16 @@ import {
|
|||||||
GRAB_THROW_BOOST_STEP,
|
GRAB_THROW_BOOST_STEP,
|
||||||
} from "@/data/grabConfig";
|
} from "@/data/grabConfig";
|
||||||
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
|
import type { ColliderShape, Vector3Tuple } from "@/types/3d";
|
||||||
|
|
||||||
interface GrabbableObjectProps {
|
interface GrabbableObjectProps {
|
||||||
position: [number, number, number];
|
position: Vector3Tuple;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
colliders?: "cuboid" | "ball" | "hull";
|
colliders?: ColliderShape;
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared mutable params — one debug folder controls all instances.
|
// Shared params let one debug folder drive every instance.
|
||||||
const params = {
|
const params = {
|
||||||
stiffness: GRAB_STIFFNESS_DEFAULT,
|
stiffness: GRAB_STIFFNESS_DEFAULT,
|
||||||
throwBoost: GRAB_THROW_BOOST_DEFAULT,
|
throwBoost: GRAB_THROW_BOOST_DEFAULT,
|
||||||
|
|||||||
@@ -10,17 +10,15 @@ import {
|
|||||||
} from "@/data/debugConfig";
|
} from "@/data/debugConfig";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
import {
|
import { InteractionManager } from "@/stateManager/InteractionManager";
|
||||||
InteractionManager,
|
|
||||||
type InteractableHandle,
|
|
||||||
type InteractableKind,
|
|
||||||
} from "@/stateManager/InteractionManager";
|
|
||||||
import { INTERACTION_RADIUS } from "@/data/interactionConfig";
|
import { INTERACTION_RADIUS } from "@/data/interactionConfig";
|
||||||
|
import type { Vector3Tuple } from "@/types/3d";
|
||||||
|
import type { InteractableHandle, InteractableKind } from "@/types/interaction";
|
||||||
|
|
||||||
interface InteractableObjectProps {
|
interface InteractableObjectProps {
|
||||||
kind: InteractableKind;
|
kind: InteractableKind;
|
||||||
label: string;
|
label: string;
|
||||||
position: [number, number, number];
|
position: Vector3Tuple;
|
||||||
bodyRef?: RefObject<RapierRigidBody | null>;
|
bodyRef?: RefObject<RapierRigidBody | null>;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
onRelease?: () => void;
|
onRelease?: () => void;
|
||||||
|
|||||||
@@ -9,21 +9,22 @@ import {
|
|||||||
TRIGGER_DEFAULT_SPAWN_OFFSET,
|
TRIGGER_DEFAULT_SPAWN_OFFSET,
|
||||||
} from "@/data/triggerConfig";
|
} from "@/data/triggerConfig";
|
||||||
import { AudioManager } from "@/stateManager/AudioManager";
|
import { AudioManager } from "@/stateManager/AudioManager";
|
||||||
|
import type { ColliderShape, Vector3Tuple } from "@/types/3d";
|
||||||
|
|
||||||
interface SpawnedModel {
|
interface SpawnedModel {
|
||||||
id: number;
|
id: number;
|
||||||
position: [number, number, number];
|
position: Vector3Tuple;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TriggerObjectProps {
|
interface TriggerObjectProps {
|
||||||
position: [number, number, number];
|
position: Vector3Tuple;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
colliders?: "cuboid" | "ball" | "hull";
|
colliders?: ColliderShape;
|
||||||
label?: string;
|
label?: string;
|
||||||
soundPath?: string;
|
soundPath?: string;
|
||||||
soundVolume?: number;
|
soundVolume?: number;
|
||||||
spawnModel?: string;
|
spawnModel?: string;
|
||||||
spawnOffset?: [number, number, number];
|
spawnOffset?: Vector3Tuple;
|
||||||
}
|
}
|
||||||
|
|
||||||
let _spawnCounter = 0;
|
let _spawnCounter = 0;
|
||||||
@@ -33,7 +34,7 @@ function SpawnedModelInstance({
|
|||||||
position,
|
position,
|
||||||
}: {
|
}: {
|
||||||
path: string;
|
path: string;
|
||||||
position: [number, number, number];
|
position: Vector3Tuple;
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const { scene } = useGLTF(path);
|
const { scene } = useGLTF(path);
|
||||||
return <primitive object={scene.clone()} position={position} />;
|
return <primitive object={scene.clone()} position={position} />;
|
||||||
@@ -64,7 +65,7 @@ export function TriggerObject({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (spawnModel) {
|
if (spawnModel) {
|
||||||
const spawnPos: [number, number, number] = [
|
const spawnPos: Vector3Tuple = [
|
||||||
position[0] + spawnOffset[0],
|
position[0] + spawnOffset[0],
|
||||||
position[1] + spawnOffset[1],
|
position[1] + spawnOffset[1],
|
||||||
position[2] + spawnOffset[2],
|
position[2] + spawnOffset[2],
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
import { useInteractionSelector } from "@/hooks/useInteraction";
|
import { useInteraction } from "@/hooks/useInteraction";
|
||||||
|
|
||||||
export function Crosshair(): React.JSX.Element | null {
|
export function Crosshair(): React.JSX.Element | null {
|
||||||
const cameraMode = useCameraMode();
|
const cameraMode = useCameraMode();
|
||||||
const focused = useInteractionSelector((state) => state.focused);
|
const { focused } = useInteraction();
|
||||||
|
|
||||||
if (cameraMode !== "player") return null;
|
if (cameraMode !== "player") return null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { INTERACT_KEY } from "@/data/keybindings";
|
import { INTERACT_KEY } from "@/data/keybindings";
|
||||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
import { useInteractionSelector } from "@/hooks/useInteraction";
|
import { useInteraction } from "@/hooks/useInteraction";
|
||||||
|
|
||||||
export function InteractPrompt(): React.JSX.Element | null {
|
export function InteractPrompt(): React.JSX.Element | null {
|
||||||
const cameraMode = useCameraMode();
|
const cameraMode = useCameraMode();
|
||||||
const focused = useInteractionSelector((state) => state.focused);
|
const { focused, holding } = useInteraction();
|
||||||
const holding = useInteractionSelector((state) => state.holding);
|
|
||||||
|
|
||||||
if (cameraMode !== "player") return null;
|
if (cameraMode !== "player") return null;
|
||||||
if (!focused || holding || focused.kind !== "trigger") return null;
|
if (!focused || holding || focused.kind !== "trigger") return null;
|
||||||
|
|||||||
@@ -11,6 +11,5 @@ export const PLAYER_XZ_DAMPING_FACTOR = 8;
|
|||||||
|
|
||||||
export const PLAYER_SPAWN_X = 0;
|
export const PLAYER_SPAWN_X = 0;
|
||||||
export const PLAYER_SPAWN_Z = 0;
|
export const PLAYER_SPAWN_Z = 0;
|
||||||
export const PLAYER_SPAWN_Y_DEFAULT = 100;
|
|
||||||
export const PLAYER_SPAWN_Y_GAME = 100;
|
export const PLAYER_SPAWN_Y_GAME = 100;
|
||||||
export const PLAYER_SPAWN_Y_PHYSICS = 3;
|
export const PLAYER_SPAWN_Y_PHYSICS = 3;
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
export const TEST_SCENE_FLOOR_POSITION: [number, number, number] = [0, -0.5, 0];
|
import type { Vector3Tuple } from "@/types/3d";
|
||||||
export const TEST_SCENE_FLOOR_SIZE: [number, number, number] = [200, 1, 200];
|
|
||||||
export const TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS: [number, number, number] =
|
|
||||||
[100, 0.5, 100];
|
|
||||||
|
|
||||||
export const TEST_SCENE_GRABBABLE_POSITION: [number, number, number] = [
|
export const TEST_SCENE_FLOOR_POSITION: Vector3Tuple = [0, -0.5, 0];
|
||||||
0, 1, -3,
|
export const TEST_SCENE_FLOOR_SIZE: Vector3Tuple = [200, 1, 200];
|
||||||
];
|
export const TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS: Vector3Tuple = [
|
||||||
export const TEST_SCENE_GRABBABLE_BOX_SIZE: [number, number, number] = [
|
100, 0.5, 100,
|
||||||
0.5, 0.5, 0.5,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const TEST_SCENE_GRABBABLE_POSITION: Vector3Tuple = [0, 1, -3];
|
||||||
|
export const TEST_SCENE_GRABBABLE_BOX_SIZE: Vector3Tuple = [0.5, 0.5, 0.5];
|
||||||
export const TEST_SCENE_GRABBABLE_COLOR = "#e07b39";
|
export const TEST_SCENE_GRABBABLE_COLOR = "#e07b39";
|
||||||
export const TEST_SCENE_GRABBABLE_ROUGHNESS = 0.6;
|
export const TEST_SCENE_GRABBABLE_ROUGHNESS = 0.6;
|
||||||
export const TEST_SCENE_GRABBABLE_METALNESS = 0.1;
|
export const TEST_SCENE_GRABBABLE_METALNESS = 0.1;
|
||||||
|
|
||||||
export const TEST_SCENE_TRIGGER_POSITION: [number, number, number] = [3, 2, -3];
|
export const TEST_SCENE_TRIGGER_POSITION: Vector3Tuple = [3, 2, -3];
|
||||||
export const TEST_SCENE_TRIGGER_SOUND_PATH = "/sounds/fa.mp3";
|
export const TEST_SCENE_TRIGGER_SOUND_PATH = "/sounds/fa.mp3";
|
||||||
export const TEST_SCENE_TRIGGER_RADIUS = 0.4;
|
export const TEST_SCENE_TRIGGER_RADIUS = 0.4;
|
||||||
export const TEST_SCENE_TRIGGER_SEGMENTS = 32;
|
export const TEST_SCENE_TRIGGER_SEGMENTS = 32;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import type { Vector3Tuple } from "@/types/3d";
|
||||||
|
|
||||||
export const TRIGGER_DEFAULT_COLLIDERS = "ball";
|
export const TRIGGER_DEFAULT_COLLIDERS = "ball";
|
||||||
export const TRIGGER_DEFAULT_LABEL = "Interagir";
|
export const TRIGGER_DEFAULT_LABEL = "Interagir";
|
||||||
export const TRIGGER_DEFAULT_SOUND_VOLUME = 1;
|
export const TRIGGER_DEFAULT_SOUND_VOLUME = 1;
|
||||||
export const TRIGGER_DEFAULT_SPAWN_OFFSET: [number, number, number] = [0, 0, 0];
|
export const TRIGGER_DEFAULT_SPAWN_OFFSET: Vector3Tuple = [0, 0, 0];
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
|
||||||
import type { CameraMode } from "@/types/debug";
|
import type { CameraMode } from "@/types/debug";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { useDebugStore } from "@/hooks/debug/useDebugStore";
|
||||||
|
|
||||||
export function useCameraMode(): CameraMode {
|
export function useCameraMode(): CameraMode {
|
||||||
const debug = Debug.getInstance();
|
return useDebugStore((debug) => debug.getCameraMode());
|
||||||
|
|
||||||
return useSyncExternalStore(
|
|
||||||
(listener) => debug.subscribe(listener),
|
|
||||||
() => debug.getCameraMode(),
|
|
||||||
() => debug.getCameraMode(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useSyncExternalStore } from "react";
|
||||||
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
|
export function useDebugStore<T>(selector: (debug: Debug) => T): T {
|
||||||
|
const debug = Debug.getInstance();
|
||||||
|
|
||||||
|
return useSyncExternalStore(
|
||||||
|
(listener) => debug.subscribe(listener),
|
||||||
|
() => selector(debug),
|
||||||
|
() => selector(debug),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
|
||||||
import type { SceneMode } from "@/types/debug";
|
import type { SceneMode } from "@/types/debug";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { useDebugStore } from "@/hooks/debug/useDebugStore";
|
||||||
|
|
||||||
export function useSceneMode(): SceneMode {
|
export function useSceneMode(): SceneMode {
|
||||||
const debug = Debug.getInstance();
|
return useDebugStore((debug) => debug.getSceneMode());
|
||||||
|
|
||||||
return useSyncExternalStore(
|
|
||||||
(listener) => debug.subscribe(listener),
|
|
||||||
() => debug.getSceneMode(),
|
|
||||||
() => debug.getSceneMode(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
import {
|
import { InteractionManager } from "@/stateManager/InteractionManager";
|
||||||
InteractionManager,
|
import type { InteractionSnapshot } from "@/types/interaction";
|
||||||
type InteractionSnapshot,
|
|
||||||
} from "@/stateManager/InteractionManager";
|
|
||||||
|
|
||||||
const manager = InteractionManager.getInstance();
|
const manager = InteractionManager.getInstance();
|
||||||
|
|
||||||
@@ -12,11 +10,3 @@ export function useInteraction(): InteractionSnapshot {
|
|||||||
manager.getState.bind(manager),
|
manager.getState.bind(manager),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInteractionSelector<T>(
|
|
||||||
selector: (state: InteractionSnapshot) => T,
|
|
||||||
): T {
|
|
||||||
return useSyncExternalStore(manager.subscribe.bind(manager), () =>
|
|
||||||
selector(manager.getState()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import type { RefObject } from "react";
|
||||||
|
import type { Object3D } from "three";
|
||||||
|
import { Octree } from "three/addons/math/Octree.js";
|
||||||
|
import type { OctreeReadyHandler } from "@/types/3d";
|
||||||
|
|
||||||
|
export function useOctreeGraphNode(
|
||||||
|
graphNodeRef: RefObject<Object3D | null>,
|
||||||
|
onOctreeReady: OctreeReadyHandler,
|
||||||
|
): void {
|
||||||
|
const octreeBuilt = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const graphNode = graphNodeRef.current;
|
||||||
|
if (octreeBuilt.current || !graphNode) return;
|
||||||
|
octreeBuilt.current = true;
|
||||||
|
|
||||||
|
graphNode.updateMatrixWorld(true);
|
||||||
|
|
||||||
|
const octree = new Octree();
|
||||||
|
octree.fromGraphNode(graphNode);
|
||||||
|
onOctreeReady(octree);
|
||||||
|
}, [graphNodeRef, onOctreeReady]);
|
||||||
|
}
|
||||||
@@ -3,6 +3,10 @@ export class AudioManager {
|
|||||||
private readonly _audioPools = new Map<string, HTMLAudioElement[]>();
|
private readonly _audioPools = new Map<string, HTMLAudioElement[]>();
|
||||||
|
|
||||||
private static readonly MAX_POOL_SIZE_PER_SOUND = 6;
|
private static readonly MAX_POOL_SIZE_PER_SOUND = 6;
|
||||||
|
private static readonly IGNORED_PLAYBACK_ERRORS = new Set([
|
||||||
|
"AbortError",
|
||||||
|
"NotAllowedError",
|
||||||
|
]);
|
||||||
|
|
||||||
static getInstance(): AudioManager {
|
static getInstance(): AudioManager {
|
||||||
if (!AudioManager._instance) {
|
if (!AudioManager._instance) {
|
||||||
@@ -19,9 +23,15 @@ export class AudioManager {
|
|||||||
audio.volume = Math.max(0, Math.min(1, volume));
|
audio.volume = Math.max(0, Math.min(1, volume));
|
||||||
audio.currentTime = 0;
|
audio.currentTime = 0;
|
||||||
|
|
||||||
void audio.play().catch(() => {
|
void audio.play().catch((error: unknown) => {
|
||||||
audio.pause();
|
if (
|
||||||
audio.currentTime = 0;
|
error instanceof DOMException &&
|
||||||
|
AudioManager.IGNORED_PLAYBACK_ERRORS.has(error.name)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`Failed to play sound: ${path}`, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
export type InteractableKind = "grab" | "trigger";
|
import type {
|
||||||
|
InteractableHandle,
|
||||||
export interface InteractableHandle {
|
InteractionSnapshot,
|
||||||
kind: InteractableKind;
|
} from "@/types/interaction";
|
||||||
label: string;
|
|
||||||
onPress: () => void;
|
|
||||||
onRelease: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InteractionSnapshot {
|
|
||||||
focused: InteractableHandle | null;
|
|
||||||
holding: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class InteractionManager {
|
export class InteractionManager {
|
||||||
private static _instance: InteractionManager | null = null;
|
private static _instance: InteractionManager | null = null;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Octree } from "three/addons/math/Octree.js";
|
||||||
|
|
||||||
|
export type Vector3Tuple = [number, number, number];
|
||||||
|
|
||||||
|
export type ColliderShape = "cuboid" | "ball" | "hull";
|
||||||
|
|
||||||
|
export type OctreeReadyHandler = (octree: Octree) => void;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export type InteractableKind = "grab" | "trigger";
|
||||||
|
|
||||||
|
export interface InteractableHandle {
|
||||||
|
kind: InteractableKind;
|
||||||
|
label: string;
|
||||||
|
onPress: () => void;
|
||||||
|
onRelease: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InteractionSnapshot {
|
||||||
|
focused: InteractableHandle | null;
|
||||||
|
holding: boolean;
|
||||||
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
type Listener<TPayload> = (payload: TPayload) => void;
|
type Listener<TPayload> = (payload: TPayload) => void;
|
||||||
|
|
||||||
// TypeScript cannot narrow mapped-type indexed access by a generic key TKey
|
|
||||||
// (microsoft/TypeScript#30581). The helper below encapsulates the one necessary
|
|
||||||
// cast so the rest of the class stays cast-free.
|
|
||||||
type ListenerMap<TEvents extends Record<string, unknown>> = {
|
type ListenerMap<TEvents extends Record<string, unknown>> = {
|
||||||
[TKey in keyof TEvents]?: Set<Listener<TEvents[TKey]>>;
|
[TKey in keyof TEvents]?: Set<Listener<TEvents[TKey]>>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Suspense, lazy } from "react";
|
import { lazy } from "react";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
const Perf = lazy(() => import("r3f-perf").then((m) => ({ default: m.Perf })));
|
const Perf = lazy(() => import("r3f-perf").then((m) => ({ default: m.Perf })));
|
||||||
@@ -10,9 +10,5 @@ export function DebugPerf(): React.JSX.Element | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <Perf position="bottom-right" />;
|
||||||
<Suspense fallback={null}>
|
|
||||||
<Perf position="bottom-right" />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-14
@@ -2,35 +2,25 @@ import { useEffect, useRef } from "react";
|
|||||||
import { useThree } from "@react-three/fiber";
|
import { useThree } from "@react-three/fiber";
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { Octree } from "three/addons/math/Octree.js";
|
|
||||||
import { MAP_DEBUG_BOX_HELPER_COLOR } from "@/data/debugConfig";
|
import { MAP_DEBUG_BOX_HELPER_COLOR } from "@/data/debugConfig";
|
||||||
|
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
||||||
|
import type { OctreeReadyHandler } from "@/types/3d";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
const MAP_PATH = "/models/map/model.gltf";
|
const MAP_PATH = "/models/map/model.gltf";
|
||||||
|
|
||||||
interface MapProps {
|
interface MapProps {
|
||||||
onOctreeReady: (octree: Octree) => void;
|
onOctreeReady: OctreeReadyHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Map({ onOctreeReady }: MapProps): React.JSX.Element {
|
export function Map({ onOctreeReady }: MapProps): React.JSX.Element {
|
||||||
const { scene: gltfScene } = useGLTF(MAP_PATH);
|
const { scene: gltfScene } = useGLTF(MAP_PATH);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const octreeBuilt = useRef(false);
|
|
||||||
const boxHelpersRef = useRef<THREE.BoxHelper[]>([]);
|
const boxHelpersRef = useRef<THREE.BoxHelper[]>([]);
|
||||||
const { scene } = useThree();
|
const { scene } = useThree();
|
||||||
|
|
||||||
useEffect(() => {
|
useOctreeGraphNode(groupRef, onOctreeReady);
|
||||||
if (octreeBuilt.current || !groupRef.current) return;
|
|
||||||
octreeBuilt.current = true;
|
|
||||||
|
|
||||||
groupRef.current.updateMatrixWorld(true);
|
|
||||||
|
|
||||||
const octree = new Octree();
|
|
||||||
octree.fromGraphNode(groupRef.current);
|
|
||||||
onOctreeReady(octree);
|
|
||||||
}, [onOctreeReady]);
|
|
||||||
|
|
||||||
// BoxHelper wireframes in debug mode — one per mesh in the model
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const debug = Debug.getInstance();
|
const debug = Debug.getInstance();
|
||||||
if (!debug.active || !groupRef.current) return;
|
if (!debug.active || !groupRef.current) return;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
|
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { Octree } from "three/addons/math/Octree.js";
|
|
||||||
import { GrabbableObject } from "@/components/3d/GrabbableObject";
|
import { GrabbableObject } from "@/components/3d/GrabbableObject";
|
||||||
import { TriggerObject } from "@/components/3d/TriggerObject";
|
import { TriggerObject } from "@/components/3d/TriggerObject";
|
||||||
import {
|
import {
|
||||||
@@ -21,27 +20,19 @@ import {
|
|||||||
TEST_SCENE_TRIGGER_SEGMENTS,
|
TEST_SCENE_TRIGGER_SEGMENTS,
|
||||||
TEST_SCENE_TRIGGER_SOUND_PATH,
|
TEST_SCENE_TRIGGER_SOUND_PATH,
|
||||||
} from "@/data/testSceneConfig";
|
} from "@/data/testSceneConfig";
|
||||||
|
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
||||||
|
import type { OctreeReadyHandler } from "@/types/3d";
|
||||||
|
|
||||||
interface TestSceneProps {
|
interface TestSceneProps {
|
||||||
onOctreeReady: (octree: Octree) => void;
|
onOctreeReady: OctreeReadyHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TestScene({
|
export function TestScene({
|
||||||
onOctreeReady,
|
onOctreeReady,
|
||||||
}: TestSceneProps): React.JSX.Element {
|
}: TestSceneProps): React.JSX.Element {
|
||||||
const floorRef = useRef<THREE.Group>(null);
|
const floorRef = useRef<THREE.Group>(null);
|
||||||
const octreeBuilt = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useOctreeGraphNode(floorRef, onOctreeReady);
|
||||||
if (octreeBuilt.current || !floorRef.current) return;
|
|
||||||
octreeBuilt.current = true;
|
|
||||||
|
|
||||||
floorRef.current.updateMatrixWorld(true);
|
|
||||||
|
|
||||||
const octree = new Octree();
|
|
||||||
octree.fromGraphNode(floorRef.current);
|
|
||||||
onOctreeReady(octree);
|
|
||||||
}, [onOctreeReady]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useThree } from "@react-three/fiber";
|
import { useThree } from "@react-three/fiber";
|
||||||
import type { Octree } from "three/addons/math/Octree.js";
|
import type { Octree } from "three/addons/math/Octree.js";
|
||||||
import {
|
import { PLAYER_SPAWN_X, PLAYER_SPAWN_Z } from "@/data/playerConfig";
|
||||||
PLAYER_SPAWN_X,
|
|
||||||
PLAYER_SPAWN_Y_DEFAULT,
|
|
||||||
PLAYER_SPAWN_Z,
|
|
||||||
} from "@/data/playerConfig";
|
|
||||||
import { PlayerCamera } from "@/world/player/PlayerCamera";
|
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,
|
octree = null,
|
||||||
spawnY = PLAYER_SPAWN_Y_DEFAULT,
|
spawnY,
|
||||||
}: PlayerComponentProps): React.JSX.Element {
|
}: PlayerComponentProps): React.JSX.Element {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
const onFloor = useRef(false);
|
const onFloor = useRef(false);
|
||||||
const wantsJump = useRef(false);
|
const wantsJump = useRef(false);
|
||||||
|
|
||||||
// Capsule: start = feet, end = eyes
|
|
||||||
const capsule = useRef(
|
const capsule = useRef(
|
||||||
new Capsule(
|
new Capsule(
|
||||||
new THREE.Vector3(0, PLAYER_CAPSULE_RADIUS, 0),
|
new THREE.Vector3(0, PLAYER_CAPSULE_RADIUS, 0),
|
||||||
@@ -69,7 +68,6 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sync capsule to camera spawn position on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const spawnY = camera.position.y;
|
const spawnY = camera.position.y;
|
||||||
capsule.current.start.set(
|
capsule.current.start.set(
|
||||||
@@ -78,8 +76,7 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
PLAYER_SPAWN_Z,
|
PLAYER_SPAWN_Z,
|
||||||
);
|
);
|
||||||
capsule.current.end.set(PLAYER_SPAWN_X, spawnY, PLAYER_SPAWN_Z);
|
capsule.current.end.set(PLAYER_SPAWN_X, spawnY, PLAYER_SPAWN_Z);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [camera]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interaction = InteractionManager.getInstance();
|
const interaction = InteractionManager.getInstance();
|
||||||
@@ -168,7 +165,6 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
const dt = Math.min(delta, PLAYER_MAX_DELTA);
|
const dt = Math.min(delta, PLAYER_MAX_DELTA);
|
||||||
|
|
||||||
// Compute wish direction from camera yaw (XZ only)
|
|
||||||
camera.getWorldDirection(_forward);
|
camera.getWorldDirection(_forward);
|
||||||
_forward.setY(0);
|
_forward.setY(0);
|
||||||
if (_forward.lengthSq() > 0) {
|
if (_forward.lengthSq() > 0) {
|
||||||
@@ -183,7 +179,6 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
if (keys.current.right) _wishDir.add(_right);
|
if (keys.current.right) _wishDir.add(_right);
|
||||||
if (_wishDir.lengthSq() > 0) _wishDir.normalize();
|
if (_wishDir.lengthSq() > 0) _wishDir.normalize();
|
||||||
|
|
||||||
// Accelerate horizontally
|
|
||||||
const accel = onFloor.current
|
const accel = onFloor.current
|
||||||
? PLAYER_WALK_SPEED
|
? PLAYER_WALK_SPEED
|
||||||
: PLAYER_WALK_SPEED * PLAYER_AIR_CONTROL_FACTOR;
|
: PLAYER_WALK_SPEED * PLAYER_AIR_CONTROL_FACTOR;
|
||||||
@@ -196,7 +191,6 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
velocity.current.x *= damping;
|
velocity.current.x *= damping;
|
||||||
velocity.current.z *= damping;
|
velocity.current.z *= damping;
|
||||||
|
|
||||||
// Gravity + jump
|
|
||||||
if (onFloor.current) {
|
if (onFloor.current) {
|
||||||
velocity.current.y = Math.max(0, velocity.current.y);
|
velocity.current.y = Math.max(0, velocity.current.y);
|
||||||
if (wantsJump.current) {
|
if (wantsJump.current) {
|
||||||
@@ -208,11 +202,9 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
}
|
}
|
||||||
wantsJump.current = false;
|
wantsJump.current = false;
|
||||||
|
|
||||||
// Move capsule
|
|
||||||
_translateVec.copy(velocity.current).multiplyScalar(dt);
|
_translateVec.copy(velocity.current).multiplyScalar(dt);
|
||||||
capsule.current.translate(_translateVec);
|
capsule.current.translate(_translateVec);
|
||||||
|
|
||||||
// Resolve collisions against octree
|
|
||||||
if (octree) {
|
if (octree) {
|
||||||
const result = octree.capsuleIntersect(capsule.current);
|
const result = octree.capsuleIntersect(capsule.current);
|
||||||
onFloor.current = false;
|
onFloor.current = false;
|
||||||
@@ -221,21 +213,18 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
onFloor.current = result.normal.y > 0;
|
onFloor.current = result.normal.y > 0;
|
||||||
|
|
||||||
if (!onFloor.current) {
|
if (!onFloor.current) {
|
||||||
// Cancel velocity component going into the wall
|
|
||||||
const vn = result.normal.dot(velocity.current);
|
const vn = result.normal.dot(velocity.current);
|
||||||
velocity.current.addScaledVector(result.normal, -vn);
|
velocity.current.addScaledVector(result.normal, -vn);
|
||||||
} else {
|
} else {
|
||||||
velocity.current.y = Math.max(0, velocity.current.y);
|
velocity.current.y = Math.max(0, velocity.current.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push capsule out of geometry
|
|
||||||
capsule.current.translate(
|
capsule.current.translate(
|
||||||
result.normal.clone().multiplyScalar(result.depth),
|
result.normal.clone().multiplyScalar(result.depth),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync camera to capsule top (eye position)
|
|
||||||
camera.position.copy(capsule.current.end);
|
camera.position.copy(capsule.current.end);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user