Files
neru.rip/src/app/fear/scene-components/player.tsx
T
2026-06-02 02:50:11 -03:00

194 lines
7.2 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 viewDirection = new THREE.Vector3();
const targetDest = new THREE.Vector3();
const playerRoot = new THREE.Vector3(0, FEAR_SETTINGS.PLAYER_HEIGHT, 0);
const targetVelocity = new THREE.Vector3();
const currentVelocity = 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 bobIntensity = useRef<number>(0);
const confirmedSegment = useRef<number>(0);
const hasTriggeredThisSegment = useRef<boolean>(false);
const footstepAudio = useRef<HTMLAudioElement[]>([]);
const hasStepped = useRef<boolean>(false);
useEffect(() => {
playerRoot.set(camera.position.x, FEAR_SETTINGS.PLAYER_HEIGHT, camera.position.z);
footstepAudio.current = Array.from({ length: 6 }, (_, i) => {
const audio = new Audio(`fear/snd/footstep${i + 1}.mp3`);
audio.volume = 0.4;
return audio;
});
}, []);
const playRandomFootstep = () => {
if (footstepAudio.current.length === 0) return;
const randomIndex = Math.floor(Math.random() * footstepAudio.current.length);
const audio = footstepAudio.current[randomIndex];
audio.currentTime = 0;
audio.play().catch((err) => {
console.warn("Footstep playback blocked by browser autocomplete/interaction rules.", err);
});
};
useFrame((state, delta) => {
const dt = Math.min(delta, 0.1);
camera.getWorldDirection(forward);
forward.y = 0;
forward.normalize();
side.crossVectors(forward, THREE.Object3D.DEFAULT_UP).normalize();
const moveForward = Number(controls.Forward) - Number(controls.Backward);
const moveSide = Number(controls.Right) - Number(controls.Left);
targetVelocity.set(0, 0, 0);
if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward);
if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide);
if (targetVelocity.lengthSq() > 0)
targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED);
currentVelocity.lerp(targetVelocity, 10 * dt);
playerRoot.x += currentVelocity.x * dt;
playerRoot.z += currentVelocity.z * dt;
const minX = -fearState.currentWidth / 2 + FEAR_SETTINGS.WALL_BUFFER;
const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER;
playerRoot.x = THREE.MathUtils.clamp(playerRoot.x, minX, maxX);
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right;
bobIntensity.current = THREE.MathUtils.lerp(bobIntensity.current, isMoving ? 1 : 0, 8 * dt);
if (isMoving)
movementCounter.current += dt * 12;
const sinWave = Math.sin(movementCounter.current);
const moveBobY = sinWave * 0.06 * bobIntensity.current;
const moveBobX = Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current;
if (isMoving && sinWave < -0.9) {
if (!hasStepped.current) {
playRandomFootstep();
hasStepped.current = true;
}
} else if (sinWave > 0) {
hasStepped.current = false;
}
const breatheTime = state.clock.elapsedTime * 1.8;
const breatheBobY = Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5);
camera.position.copy(playerRoot);
camera.position.y += moveBobY + breatheBobY;
camera.position.addScaledVector(side, moveBobX);
if (flashlightRef.current) {
flashlightRef.current.position.lerp(camera.position, 7 * dt);
camera.getWorldDirection(viewDirection);
targetDest
.copy(camera.position)
.addScaledVector(viewDirection, 10);
flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
flashlightRef.current.target.updateMatrixWorld();
flashlightRef.current.intensity =
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
Math.sin(state.clock.elapsedTime * 30) * 0.15 * Math.cos(state.clock.elapsedTime * 3);
}
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
const absoluteZ = -playerRoot.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={25}
angle={0.35}
penumbra={0.8}
intensity={0}
color="#fffaed"
decay={2}
castShadow
shadow-bias={-0.001}
/>
</>
);
}