142 lines
5.5 KiB
TypeScript
142 lines
5.5 KiB
TypeScript
import { useFrame, useThree } from "@react-three/fiber";
|
|
import { useEffect, useRef } from "react";
|
|
import { FEAR_SETTINGS, fearState } from "../state";
|
|
import { PointerLockControls } from "@react-three/drei";
|
|
|
|
import * as THREE from "three";
|
|
|
|
const forward = new THREE.Vector3();
|
|
const side = new THREE.Vector3();
|
|
const direction = new THREE.Vector3();
|
|
const viewDirection = new THREE.Vector3();
|
|
|
|
function usePlayerControls() {
|
|
const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false });
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = true;
|
|
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = true;
|
|
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = true;
|
|
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = true;
|
|
};
|
|
|
|
const handleKeyUp = (e: KeyboardEvent) => {
|
|
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = false;
|
|
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = false;
|
|
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = false;
|
|
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = false;
|
|
};
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
window.addEventListener('keyup', handleKeyUp);
|
|
|
|
return () => {
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
window.removeEventListener('keyup', handleKeyUp);
|
|
};
|
|
}, []);
|
|
|
|
return keys.current;
|
|
}
|
|
|
|
export default function Player() {
|
|
const { camera } = useThree();
|
|
const controls = usePlayerControls();
|
|
|
|
const flashlightRef = useRef<THREE.SpotLight>(null);
|
|
const movementCounter = useRef<number>(0);
|
|
|
|
const confirmedSegment = useRef<number>(0);
|
|
const hasTriggeredThisSegment = useRef<boolean>(false);
|
|
|
|
useFrame((state, delta) => {
|
|
camera.getWorldDirection(forward);
|
|
forward.y = 0;
|
|
forward.normalize();
|
|
|
|
side.crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
|
|
|
|
const moveForward = Number(controls.Forward) - Number(controls.Backward);
|
|
const moveSide = Number(controls.Right) - Number(controls.Left);
|
|
|
|
direction.set(0, 0, 0);
|
|
if (moveForward !== 0) direction.addScaledVector(forward, moveForward);
|
|
if (moveSide !== 0) direction.addScaledVector(side, moveSide);
|
|
|
|
if (direction.lengthSq() > 0)
|
|
direction.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED * delta);
|
|
|
|
camera.position.x += direction.x;
|
|
camera.position.z += direction.z;
|
|
|
|
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right;
|
|
if (isMoving) {
|
|
movementCounter.current += delta * 10;
|
|
camera.position.y = FEAR_SETTINGS.PLAYER_HEIGHT + Math.sin(movementCounter.current) * 0.08;
|
|
camera.position.x += Math.cos(movementCounter.current / 2) * 0.006;
|
|
} else {
|
|
const breatheTime = state.clock.elapsedTime * 1.5;
|
|
const breatheY = FEAR_SETTINGS.PLAYER_HEIGHT + Math.sin(breatheTime) * 0.1;
|
|
camera.position.y = THREE.MathUtils.lerp(camera.position.y, breatheY, 4 * delta);
|
|
}
|
|
|
|
if (flashlightRef.current) {
|
|
flashlightRef.current.position.copy(camera.position);
|
|
camera.getWorldDirection(viewDirection);
|
|
|
|
const targetDest = new THREE.Vector3()
|
|
.copy(camera.position)
|
|
.addScaledVector(viewDirection, 10);
|
|
|
|
flashlightRef.current.target.position.lerp(targetDest, 10 * delta);
|
|
flashlightRef.current.target.updateMatrixWorld();
|
|
|
|
flashlightRef.current.intensity = FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE + Math.sin(state.clock.elapsedTime * 30) * 0.3;
|
|
}
|
|
|
|
const minX = -fearState.currentWidth / 2 + FEAR_SETTINGS.WALL_BUFFER;
|
|
const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER;
|
|
|
|
if (camera.position.x < minX) camera.position.x = minX;
|
|
if (camera.position.x > maxX) camera.position.x = maxX;
|
|
|
|
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
|
|
const absoluteZ = -camera.position.z;
|
|
const rawSegmentIndex = Math.floor(absoluteZ / length);
|
|
const progressZ = ((absoluteZ % length) + length) % length / length;
|
|
|
|
if (rawSegmentIndex > confirmedSegment.current && progressZ > 0.25) {
|
|
if (!hasTriggeredThisSegment.current) {
|
|
fearState.registerLoop('forward');
|
|
hasTriggeredThisSegment.current = true;
|
|
}
|
|
confirmedSegment.current = rawSegmentIndex;
|
|
}
|
|
else if (rawSegmentIndex < confirmedSegment.current && progressZ < 0.75) {
|
|
if (!hasTriggeredThisSegment.current) {
|
|
fearState.registerLoop('backward');
|
|
hasTriggeredThisSegment.current = true;
|
|
}
|
|
confirmedSegment.current = rawSegmentIndex;
|
|
}
|
|
|
|
if (rawSegmentIndex === confirmedSegment.current && progressZ > 0.35 && progressZ < 0.65)
|
|
hasTriggeredThisSegment.current = false;
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<PointerLockControls />
|
|
<spotLight
|
|
ref={flashlightRef}
|
|
distance={22}
|
|
angle={0.35}
|
|
penumbra={0.7}
|
|
intensity={5}
|
|
color="#fffaed"
|
|
/>
|
|
</>
|
|
);
|
|
}
|