refacto : cleaning the codebasebase again

This commit is contained in:
2026-04-19 16:50:11 +02:00
parent f9c4495610
commit dcbc1c73f5
26 changed files with 127 additions and 5726 deletions
+2 -2
View File
@@ -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
-5589
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -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",
+4 -3
View File
@@ -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,
+4 -6
View File
@@ -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;
+7 -6
View File
@@ -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],
+2 -2
View File
@@ -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;
+2 -3
View File
@@ -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;
-1
View File
@@ -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;
+9 -10
View File
@@ -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;
+3 -1
View File
@@ -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];
+2 -9
View File
@@ -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(),
);
} }
+12
View File
@@ -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),
);
}
+2 -9
View File
@@ -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(),
);
} }
+2 -12
View File
@@ -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()),
);
}
+24
View File
@@ -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]);
}
+13 -3
View File
@@ -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);
}); });
} }
+4 -13
View File
@@ -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;
+7
View File
@@ -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;
+13
View File
@@ -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;
}
-3
View File
@@ -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]>>;
}; };
+2 -6
View File
@@ -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
View File
@@ -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;
+5 -14
View File
@@ -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 (
<> <>
+3 -7
View File
@@ -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);
+1 -12
View File
@@ -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);
}); });