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(null); const movementCounter = useRef(0); const bobIntensity = useRef(0); const confirmedSegment = useRef(0); const hasTriggeredThisSegment = useRef(false); const footstepAudio = useRef([]); const hasStepped = useRef(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.copy(camera.position); 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 ( <> ); }