Files
La-Fabrik/src/world/player/PlayerController.tsx
T
2026-04-27 11:14:43 +02:00

235 lines
6.5 KiB
TypeScript

import { useEffect, useRef } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import * as THREE from "three";
import { Capsule } from "three/addons/math/Capsule.js";
import type { Octree } from "three/addons/math/Octree.js";
import {
INTERACT_KEY,
JUMP_KEY,
MOVE_BACKWARD_KEY,
MOVE_FORWARD_KEY,
MOVE_LEFT_KEY,
MOVE_RIGHT_KEY,
PRIMARY_INTERACT_MOUSE_BUTTON,
} from "@/data/keybindings";
import {
PLAYER_ACCELERATION_MULTIPLIER,
PLAYER_AIR_CONTROL_FACTOR,
PLAYER_CAPSULE_RADIUS,
PLAYER_EYE_HEIGHT,
PLAYER_GRAVITY,
PLAYER_JUMP_SPEED,
PLAYER_MAX_DELTA,
PLAYER_WALK_SPEED,
PLAYER_XZ_DAMPING_FACTOR,
} from "@/data/playerConfig";
import { InteractionManager } from "@/stateManager/InteractionManager";
import type { Vector3Tuple } from "@/types/3d";
type Keys = {
forward: boolean;
backward: boolean;
left: boolean;
right: boolean;
jump: boolean;
};
const DEFAULT_KEYS: Keys = {
forward: false,
backward: false,
left: false,
right: false,
jump: false,
};
interface PlayerControllerProps {
octree: Octree | null;
spawnPosition: Vector3Tuple;
}
const _forward = new THREE.Vector3();
const _right = new THREE.Vector3();
const _wishDir = new THREE.Vector3();
const _up = new THREE.Vector3(0, 1, 0);
const _translateVec = new THREE.Vector3();
const _collisionCorrection = new THREE.Vector3();
export function PlayerController({
octree,
spawnPosition,
}: PlayerControllerProps): null {
const camera = useThree((state) => state.camera);
const keys = useRef<Keys>({ ...DEFAULT_KEYS });
const velocity = useRef(new THREE.Vector3());
const onFloor = useRef(false);
const wantsJump = useRef(false);
const capsule = useRef(
new Capsule(
new THREE.Vector3(0, PLAYER_CAPSULE_RADIUS, 0),
new THREE.Vector3(0, PLAYER_EYE_HEIGHT - PLAYER_CAPSULE_RADIUS, 0),
PLAYER_CAPSULE_RADIUS,
),
);
useEffect(() => {
capsule.current.start.set(
spawnPosition[0],
spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS,
spawnPosition[2],
);
capsule.current.end.set(...spawnPosition);
velocity.current.set(0, 0, 0);
onFloor.current = false;
wantsJump.current = false;
camera.position.copy(capsule.current.end);
}, [camera, spawnPosition]);
useEffect(() => {
const interaction = InteractionManager.getInstance();
const handleKeyDown = (event: KeyboardEvent): void => {
switch (event.key.toLowerCase()) {
case MOVE_FORWARD_KEY:
keys.current.forward = true;
break;
case MOVE_BACKWARD_KEY:
keys.current.backward = true;
break;
case MOVE_LEFT_KEY:
keys.current.left = true;
break;
case MOVE_RIGHT_KEY:
keys.current.right = true;
break;
case JUMP_KEY:
wantsJump.current = true;
break;
case INTERACT_KEY:
if (interaction.getState().focused?.kind === "trigger") {
interaction.pressInteract();
}
break;
default:
return;
}
event.preventDefault();
};
const handleKeyUp = (event: KeyboardEvent): void => {
switch (event.key.toLowerCase()) {
case MOVE_FORWARD_KEY:
keys.current.forward = false;
break;
case MOVE_BACKWARD_KEY:
keys.current.backward = false;
break;
case MOVE_LEFT_KEY:
keys.current.left = false;
break;
case MOVE_RIGHT_KEY:
keys.current.right = false;
break;
default:
return;
}
event.preventDefault();
};
const handleMouseDown = (event: MouseEvent): void => {
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
if (interaction.getState().focused?.kind === "grab") {
interaction.pressInteract();
}
};
const handleMouseUp = (event: MouseEvent): void => {
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
if (interaction.getState().holding) {
interaction.releaseInteract();
}
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
document.addEventListener("mousedown", handleMouseDown);
document.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
document.removeEventListener("mousedown", handleMouseDown);
document.removeEventListener("mouseup", handleMouseUp);
keys.current = { ...DEFAULT_KEYS };
};
}, []);
useFrame((_, delta) => {
const dt = Math.min(delta, PLAYER_MAX_DELTA);
camera.getWorldDirection(_forward);
_forward.setY(0);
if (_forward.lengthSq() > 0) {
_forward.normalize();
_right.crossVectors(_forward, _up).normalize();
}
_wishDir.set(0, 0, 0);
if (keys.current.forward) _wishDir.add(_forward);
if (keys.current.backward) _wishDir.sub(_forward);
if (keys.current.left) _wishDir.sub(_right);
if (keys.current.right) _wishDir.add(_right);
if (_wishDir.lengthSq() > 0) _wishDir.normalize();
const accel = onFloor.current
? PLAYER_WALK_SPEED
: PLAYER_WALK_SPEED * PLAYER_AIR_CONTROL_FACTOR;
velocity.current.x +=
_wishDir.x * accel * dt * PLAYER_ACCELERATION_MULTIPLIER;
velocity.current.z +=
_wishDir.z * accel * dt * PLAYER_ACCELERATION_MULTIPLIER;
const damping = Math.exp(-PLAYER_XZ_DAMPING_FACTOR * dt);
velocity.current.x *= damping;
velocity.current.z *= damping;
if (onFloor.current) {
velocity.current.y = Math.max(0, velocity.current.y);
if (wantsJump.current) {
velocity.current.y = PLAYER_JUMP_SPEED;
onFloor.current = false;
}
} else {
velocity.current.y -= PLAYER_GRAVITY * dt;
}
wantsJump.current = false;
_translateVec.copy(velocity.current).multiplyScalar(dt);
capsule.current.translate(_translateVec);
if (octree) {
const result = octree.capsuleIntersect(capsule.current);
onFloor.current = false;
if (result) {
onFloor.current = result.normal.y > 0;
if (!onFloor.current) {
const vn = result.normal.dot(velocity.current);
velocity.current.addScaledVector(result.normal, -vn);
} else {
velocity.current.y = Math.max(0, velocity.current.y);
}
capsule.current.translate(
_collisionCorrection.copy(result.normal).multiplyScalar(result.depth),
);
}
}
camera.position.copy(capsule.current.end);
});
return null;
}