diff --git a/public/fear/img/creature.png b/public/fear/img/creature.png new file mode 100644 index 0000000..63329b5 Binary files /dev/null and b/public/fear/img/creature.png differ diff --git a/src/app/fear/page.tsx b/src/app/fear/page.tsx index 5a61996..a633de2 100644 --- a/src/app/fear/page.tsx +++ b/src/app/fear/page.tsx @@ -2,489 +2,37 @@ import './page.css'; -import { PointerLockControls, useTexture } from "@react-three/drei"; -import { Canvas, useFrame, useThree } from "@react-three/fiber"; -import { BrightnessContrast, EffectComposer, Noise, Pixelation, Vignette } from "@react-three/postprocessing"; -import { Suspense, useEffect, useRef, useState } from "react"; +import { Canvas } from "@react-three/fiber"; +import { BrightnessContrast, EffectComposer, HueSaturation, Noise, Pixelation, Vignette } from "@react-three/postprocessing"; +import { Suspense, useEffect, useState } from "react"; -import * as THREE from "three"; import { AmbientSound } from './scene-components/ambient-sound'; -import { fearState, FEAR_SETTINGS } from './state'; +import { fearState } from './state'; -function Player() { - const { camera } = useThree(); - const controls = usePlayerControls(); +import TheCreature from './scene-components/creature'; +import Player from './scene-components/player'; +import Hallway from './scene-components/hallway'; - const flashlightRef = useRef(null); - const movementCounter = useRef(0); - - const forward = new THREE.Vector3(); - const side = new THREE.Vector3(); - const direction = new THREE.Vector3(); - const viewDirection = new THREE.Vector3(); - - 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(4 * 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 = 5 + 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; - - if (camera.position.z < -FEAR_SETTINGS.HALLWAY_LENGTH) { - camera.position.z += FEAR_SETTINGS.HALLWAY_LENGTH; - fearState.registerLoop('forward'); - } - if (camera.position.z > 0) { - camera.position.z -= FEAR_SETTINGS.HALLWAY_LENGTH; - fearState.registerLoop('backward'); - } - }); - - return ( - <> - - - - ); -} - -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; -} - -interface DoorProps { - position: [number, number, number]; - rotation: [number, number, number]; -} -function Door({ position, rotation }: DoorProps) { - return ( - - - - - - - - - - - - - - - - - ); -} - -function Hallway() { - const [width, setWidth] = useState(fearState.currentWidth); - const [floorTex, wallTex, rustTex] = useTexture([ - 'fear/img/concrete-floor.png', - 'fear/img/concrete-wall.png', - 'fear/img/rust.png' - ]); - - [floorTex, wallTex, rustTex].forEach((tex, index) => { - tex.wrapS = tex.wrapT = THREE.RepeatWrapping; - tex.minFilter = tex.magFilter = THREE.NearestFilter; - tex.colorSpace = THREE.SRGBColorSpace; - tex.repeat.set(index === 0 ? 1 : 10, index === 0 ? 10 : 1); - }); - - const segments = [0, -1, 1, -2, 2]; - - const lightRef1 = useRef(null); - const lightRef2 = useRef(null); - const matRef1 = useRef(null); - const matRef2 = useRef(null); - const lightState = useRef<'normal' | 'flickering' | 'dead'>('normal'); - const stateEndTime = useRef(0); - const nextEventTime = useRef(5); - - const segmentsRef = useRef([]); - - const wallMaterialsRef = useRef([]); - const floorMaterialsRef = useRef([]); - const pipeMaterialsRef = useRef([]); - const bracketMaterialsRef = useRef([]); - - wallMaterialsRef.current = []; - floorMaterialsRef.current = []; - pipeMaterialsRef.current = []; - bracketMaterialsRef.current = []; - - const [isRustActive, setIsRustActive] = useState(fearState.isRustActive); +function PostProcessing() { + const [getCaught, setCaught] = useState(fearState.wasCaught); useEffect(() => { const unsubscribe = fearState.subscribe(() => { - setWidth(fearState.currentWidth); - setIsRustActive(fearState.isRustActive); + setCaught(fearState.wasCaught); }); return () => unsubscribe(); }, []); - useFrame((state, delta) => { - const time = state.clock.elapsedTime; - - fearState.update(delta); - const currentRust = fearState.isRustActive; - if (currentRust !== isRustActive) - setIsRustActive(currentRust); - - /* - lights - */ - let intensity1 = 0.85 + Math.sin(time * 2) * 0.03; - if (time > nextEventTime.current && lightState.current === 'normal') { - lightState.current = 'flickering'; - stateEndTime.current = time + 1.5 + Math.random() * 2; - } - if (lightState.current === 'flickering') { - if (time > stateEndTime.current) { - if (Math.random() > 0.4) { - lightState.current = 'dead'; - stateEndTime.current = time + 1.0 + Math.random() * 2.5; - } else { - lightState.current = 'normal'; - nextEventTime.current = time + 10 + Math.random() * 20; - } - } else { - const baseWave = Math.sin(time * 45) * 0.4 + Math.sin(time * 90) * 0.3; - intensity1 = 0.5 + baseWave; - if (Math.sin(time * 150) + Math.cos(time * 220) > 1.2) intensity1 *= Math.random() > 0.5 ? 0.0 : 0.15; - } - } - if (lightState.current === 'dead') { - if (time > stateEndTime.current) { - lightState.current = 'normal'; - nextEventTime.current = time + 12 + Math.random() * 15; - } else - intensity1 = Math.random() > 0.98 ? 0.08 : 0.0; - } - let intensity2 = lightState.current === 'dead' ? 0 : intensity1 * (0.7 + Math.sin(time * 3) * 0.1); - - if (lightRef1.current) lightRef1.current.intensity = intensity1 * 1.2; - if (matRef1.current) { - matRef1.current.emissiveIntensity = intensity1 * 2.5; - if (lightState.current !== 'normal') matRef1.current.emissive.setHSL(0.07, 0.4, Math.min(intensity1, 0.7)); - else matRef1.current.emissive.setHex(0xa8a1a1); - } - if (lightRef2.current) lightRef2.current.intensity = intensity2 * 0.6; - if (matRef2.current) matRef2.current.emissiveIntensity = intensity2 * 1.5; - - /* - walls - */ - segmentsRef.current.forEach((segGroup) => { - if (!segGroup) return; - - const leftWallGroup = segGroup.getObjectByName("left-wall-group"); - if (leftWallGroup) leftWallGroup.position.x = -width / 2; - - const rightWallGroup = segGroup.getObjectByName("right-wall-group"); - if (rightWallGroup) rightWallGroup.position.x = width / 2; - - const floorMesh = segGroup.getObjectByName("floor-mesh"); - if (floorMesh) floorMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH; - - const ceilingMesh = segGroup.getObjectByName("ceiling-mesh"); - if (ceilingMesh) ceilingMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH; - - for (let i = 0; i < 3; i++) { - const pipe = segGroup.getObjectByName(`pipe-${i}`); - if (pipe) pipe.position.x = -width / 2 + 0.4 + (i * 0.20); - } - const bracketGroup = segGroup.getObjectByName("brackets-group"); - if (bracketGroup) { - bracketGroup.children.forEach(b => { - b.position.x = -width / 2 + 0.6; - }); - } - }); - - /* - rust - */ - wallMaterialsRef.current.forEach(mat => { - if (!mat) return; - const targetTex = isRustActive ? rustTex : wallTex; - if (mat.map !== targetTex) { - mat.map = targetTex; - mat.needsUpdate = true; - } - if (isRustActive) { - mat.color.set("#918a87"); - mat.roughness = 0.95; - mat.metalness = 0.05; - } else { - mat.color.set("#ffffff"); - mat.roughness = 0.7; - mat.metalness = 0.1; - } - }); - - floorMaterialsRef.current.forEach(mat => { - if (!mat) return; - const targetTex = isRustActive ? rustTex : floorTex; - if (mat.map !== targetTex) { - mat.map = targetTex; - mat.needsUpdate = true; - } - if (isRustActive) { - mat.color.set("#8b827f"); - mat.roughness = 0.95; - mat.metalness = 0.05; - } else { - mat.color.set("#ffffff"); - mat.roughness = 0.8; - mat.metalness = 0.2; - } - }); - - pipeMaterialsRef.current.forEach(mat => { - if (!mat) return; - mat.color.set(isRustActive ? "#3d1b0f" : "#a5aca8"); - mat.roughness = isRustActive ? 0.95 : 0.0; - mat.metalness = isRustActive ? 0.05 : 0.4; - }); - - bracketMaterialsRef.current.forEach(mat => { - if (!mat) return; - mat.color.set(isRustActive ? "#1b0b05" : "#a5aca8"); - mat.roughness = isRustActive ? 0.95 : 0.0; - mat.metalness = isRustActive ? 0.05 : 0.4; - }); - }); - - return ( - <> - - - {segments.map((segmentValue, index) => ( - /* - lights - */ - { if (el) segmentsRef.current[index] = el; }} - position={[0, 0, segmentValue * FEAR_SETTINGS.HALLWAY_LENGTH]} - > - - - - - - - - - {/* floor */} - - - el && floorMaterialsRef.current.push(el)} - map={floorTex} - roughness={0.8} - metalness={0.2} - /> - - - {/* ceiling */} - - - el && floorMaterialsRef.current.push(el)} - map={floorTex} - roughness={0.8} - metalness={0.2} - /> - - - {/* l wall */} - - - - el && wallMaterialsRef.current.push(el)} - map={wallTex} - roughness={0.7} - metalness={0.1} - /> - - {!isRustActive && ( - <> - - - - )} - - - {/* r wall */} - - - - el && wallMaterialsRef.current.push(el)} - map={wallTex} - roughness={0.7} - metalness={0.1} - /> - - {!isRustActive && ( - - )} - - - {/* pipes */} - {Array.from({ length: 3 }).map((_, idx) => ( - - - el && pipeMaterialsRef.current.push(el)} - color="#a5aca8" - roughness={0.0} - metalness={0.4} - /> - - ))} - - {/* brackets */} - - {Array.from({ length: 5 }).map((_, idx) => { - const zOffset = -(idx * 8 + 4); - return ( - - - el && bracketMaterialsRef.current.push(el)} - color="#a5aca8" - roughness={0.0} - metalness={0.4} - /> - - ); - })} - - - ))} - - ); -} - -function PostProcessing() { return ( - + - + + ) } @@ -509,16 +57,17 @@ export default function Fear() { + + + + + - - - - ) } \ No newline at end of file diff --git a/src/app/fear/scene-components/creature.tsx b/src/app/fear/scene-components/creature.tsx new file mode 100644 index 0000000..5700452 --- /dev/null +++ b/src/app/fear/scene-components/creature.tsx @@ -0,0 +1,85 @@ +import { useTexture } from "@react-three/drei"; +import { useFrame, useThree } from "@react-three/fiber"; +import { useEffect, useRef, useState } from "react"; + +import * as THREE from "three"; +import { fearState } from "../state"; + +export default function TheCreature() { + const texture = useTexture('fear/img/creature.png'); + const meshRef = useRef(null); + const { camera } = useThree(); + + const [hasTriggered, setHasTriggered] = useState(false); + const [isSpawned, setIsSpawned] = useState(false); + + const speed = 15; + const globalDistance = useRef(32); + const [currentLoop, setCurrentLoop] = useState(fearState.loopCount); + + useEffect(() => { + const unsubscribe = fearState.subscribe(() => { + setCurrentLoop(fearState.loopCount); + + if (fearState.loopCount < 4) { + setIsSpawned(false); + setHasTriggered(false); + globalDistance.current = 32; + } + }); + return () => unsubscribe(); + }, []); + + useFrame((state, delta) => { + if (fearState.loopCount < 4) return; + + const creature = meshRef.current; + if (!creature) return; + + if (!isSpawned) { + setIsSpawned(true); + globalDistance.current = 32; + } + + if (!hasTriggered) + if (globalDistance.current < 40) setHasTriggered(true); + + if (hasTriggered) { + globalDistance.current -= speed * delta; + + const shakeIntensity = Math.max(0, 1 - (globalDistance.current / 32)) * 0.22; + camera.position.x += (Math.random() - 0.5) * shakeIntensity; + camera.position.y += (Math.random() - 0.5) * shakeIntensity; + + if (globalDistance.current <= 0.1) { + window.location.href = '/'; + fearState.registerCaught(); + return; + } + } + + const forwardVector = new THREE.Vector3(); + camera.getWorldDirection(forwardVector); + const lookDirZ = forwardVector.z < 0 ? -1 : 1; + + const calculatedZ = camera.position.z + (lookDirZ * globalDistance.current); + + creature.position.set(0, 1.6, calculatedZ); + creature.lookAt(camera.position.x, creature.position.y, camera.position.z); + }); + + return ( + = 4} + > + + + + ); +} diff --git a/src/app/fear/scene-components/hallway.tsx b/src/app/fear/scene-components/hallway.tsx new file mode 100644 index 0000000..d5cc430 --- /dev/null +++ b/src/app/fear/scene-components/hallway.tsx @@ -0,0 +1,369 @@ +import { useEffect, useRef, useState } from "react"; +import { FEAR_SETTINGS, fearState } from "../state"; +import { useTexture } from "@react-three/drei"; + +import * as THREE from "three"; +import { useFrame } from "@react-three/fiber"; + +interface DoorProps { + position: [number, number, number]; + rotation: [number, number, number]; +} +function Door({ position, rotation }: DoorProps) { + return ( + + + + + + + + + + + + + + + + + ); +} + +export default function Hallway() { + const [width, setWidth] = useState(fearState.currentWidth); + const [floorTex, wallTex, rustTex] = useTexture([ + 'fear/img/concrete-floor.png', + 'fear/img/concrete-wall.png', + 'fear/img/rust.png' + ]); + + useEffect(() => { + [floorTex, wallTex, rustTex].forEach((tex) => { + tex.wrapS = tex.wrapT = THREE.RepeatWrapping; + tex.minFilter = tex.magFilter = THREE.NearestFilter; + tex.colorSpace = THREE.SRGBColorSpace; + }); + }, [floorTex, wallTex, rustTex]); + + const segmentPool = [0, 1, 2, 3, 4]; + const segmentCount = segmentPool.length; + + const lightRefs = useRef<(THREE.PointLight | null)[]>([]); + const matRefs = useRef<(THREE.MeshStandardMaterial | null)[]>([]); + + const lightState = useRef<'normal' | 'flickering' | 'dead'>('normal'); + const stateEndTime = useRef(0); + const nextEventTime = useRef(5); + + const segmentsRef = useRef([]); + const wallMaterialsRef = useRef([]); + const floorMaterialsRef = useRef([]); + const pipeMaterialsRef = useRef([]); + const bracketMaterialsRef = useRef([]); + + wallMaterialsRef.current = []; + floorMaterialsRef.current = []; + pipeMaterialsRef.current = []; + bracketMaterialsRef.current = []; + + const [isRustActive, setIsRustActive] = useState(fearState.isRustActive); + + useEffect(() => { + const unsubscribe = fearState.subscribe(() => { + setWidth(fearState.currentWidth); + setIsRustActive(fearState.isRustActive); + }); + return () => unsubscribe(); + }, []); + + useFrame((state, delta) => { + const time = state.clock.elapsedTime; + + fearState.update(delta); + if (fearState.isRustActive !== isRustActive) + setIsRustActive(fearState.isRustActive); + + /* + lights + */ + let intensity1 = 0.85 + Math.sin(time * 2) * 0.03; + if (time > nextEventTime.current && lightState.current === 'normal') { + lightState.current = 'flickering'; + stateEndTime.current = time + 1.5 + Math.random() * 2; + } + if (lightState.current === 'flickering') { + if (time > stateEndTime.current) { + if (Math.random() > 0.4) { + lightState.current = 'dead'; + stateEndTime.current = time + 1.0 + Math.random() * 2.5; + } else { + lightState.current = 'normal'; + nextEventTime.current = time + 10 + Math.random() * 20; + } + } else { + const baseWave = Math.sin(time * 45) * 0.4 + Math.sin(time * 90) * 0.3; + intensity1 = 0.5 + baseWave; + if (Math.sin(time * 150) + Math.cos(time * 220) > 1.2) intensity1 *= Math.random() > 0.5 ? 0.0 : 0.15; + } + } + if (lightState.current === 'dead') { + if (time > stateEndTime.current) { + lightState.current = 'normal'; + nextEventTime.current = time + 12 + Math.random() * 15; + } else { + intensity1 = Math.random() > 0.98 ? 0.08 : 0.0; + } + } + + /* + objects + */ + const length = FEAR_SETTINGS.HALLWAY_LENGTH; + const playerSegmentZ = Math.floor(state.camera.position.z / length); + + const horizontalTexRepeat = width / FEAR_SETTINGS.HALLWAY_WIDTH; + floorTex.repeat.set(horizontalTexRepeat, 10); + wallTex.repeat.set(10, 1); + rustTex.repeat.set(horizontalTexRepeat, 10); + + floorTex.needsUpdate = true; + wallTex.needsUpdate = true; + rustTex.needsUpdate = true; + + let closestPoolIndex = 0; + let minDistance = Infinity; + + segmentsRef.current.forEach((segGroup, poolIndex) => { + if (!segGroup) return; + + let segmentZIndex = poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ; + segGroup.position.z = segmentZIndex * length; + + // Track which pool index is currently physically closest to the player's camera position + const distance = Math.abs(segGroup.position.z - state.camera.position.z); + if (distance < minDistance) { + minDistance = distance; + closestPoolIndex = poolIndex; + } + + const leftWallGroup = segGroup.getObjectByName("left-wall-group"); + if (leftWallGroup) leftWallGroup.position.x = -width / 2; + + const rightWallGroup = segGroup.getObjectByName("right-wall-group"); + if (rightWallGroup) rightWallGroup.position.x = width / 2; + + const floorMesh = segGroup.getObjectByName("floor-mesh"); + if (floorMesh) floorMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH; + + const ceilingMesh = segGroup.getObjectByName("ceiling-mesh"); + if (ceilingMesh) ceilingMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH; + + for (let i = 0; i < 3; i++) { + const pipe = segGroup.getObjectByName(`pipe-${i}`); + if (pipe) pipe.position.x = -width / 2 + 0.4 + (i * 0.20); + } + const bracketGroup = segGroup.getObjectByName("brackets-group"); + if (bracketGroup) { + bracketGroup.children.forEach(b => { + b.position.x = -width / 2 + 0.6; + }); + } + }); + + /* + dyn light + */ + segmentPool.forEach((poolIndex) => { + const light = lightRefs.current[poolIndex]; + const mat = matRefs.current[poolIndex]; + + if (poolIndex === closestPoolIndex) { + if (light) light.intensity = intensity1 * 1.2; + if (mat) { + mat.emissiveIntensity = intensity1 * 2.5; + if (lightState.current !== 'normal') mat.emissive.setHSL(0.07, 0.4, Math.min(intensity1, 0.7)); + else mat.emissive.setHex(0xa8a1a1); + } + } else { + if (light) light.intensity = 0.9; + if (mat) { + mat.emissiveIntensity = 0.8; + mat.emissive.setHex(0xa8a1a1); + } + } + }); + + /* + materials + */ + const updateMaterials = (materials: THREE.MeshStandardMaterial[], defaultTex: THREE.Texture, activeColor: string, defaultColor: string, activeRough: number, defaultRough: number, activeMetal: number, defaultMetal: number) => { + materials.forEach(mat => { + if (!mat) return; + const targetTex = isRustActive ? rustTex : defaultTex; + if (mat.map !== targetTex) { + mat.map = targetTex; + mat.needsUpdate = true; + } + mat.color.set(isRustActive ? activeColor : defaultColor); + mat.roughness = isRustActive ? activeRough : defaultRough; + mat.metalness = isRustActive ? activeMetal : defaultMetal; + }); + }; + + updateMaterials(wallMaterialsRef.current, wallTex, "#918a87", "#ffffff", 0.95, 0.7, 0.05, 0.1); + updateMaterials(floorMaterialsRef.current, floorTex, "#8b827f", "#ffffff", 0.95, 0.8, 0.05, 0.2); + + pipeMaterialsRef.current.forEach(mat => { + if (!mat) return; + mat.color.set(isRustActive ? "#3d1b0f" : "#a5aca8"); + mat.roughness = isRustActive ? 0.95 : 0.0; + mat.metalness = isRustActive ? 0.05 : 0.4; + }); + + bracketMaterialsRef.current.forEach(mat => { + if (!mat) return; + mat.color.set(isRustActive ? "#1b0b05" : "#a5aca8"); + mat.roughness = isRustActive ? 0.95 : 0.0; + mat.metalness = isRustActive ? 0.05 : 0.4; + }); + }); + + return ( + <> + + + {segmentPool.map((poolIndex) => ( + { if (el) segmentsRef.current[poolIndex] = el; }} + position={[0, 0, 0]} + > + {/* lights */} + + { lightRefs.current[poolIndex] = el; }} + intensity={0.9} + distance={15} + color="#a8a1a1" + /> + + + { matRefs.current[poolIndex] = el; }} + color="#111111" + emissive="#a8a1a1" + emissiveIntensity={0.8} + roughness={0.9} + /> + + + + {/* floor */} + + + { if (el) floorMaterialsRef.current.push(el); }} + map={floorTex} + roughness={0.8} + metalness={0.2} + /> + + + {/* ceiling */} + + + { if (el) floorMaterialsRef.current.push(el); }} + map={floorTex} + roughness={0.8} + metalness={0.2} + /> + + + {/* left wall */} + + + + { if (el) wallMaterialsRef.current.push(el); }} + map={wallTex} + roughness={0.7} + metalness={0.1} + /> + + {!isRustActive && ( + <> + + + + )} + + + {/* right wall */} + + + + { if (el) wallMaterialsRef.current.push(el); }} + map={wallTex} + roughness={0.7} + metalness={0.1} + /> + + {!isRustActive && ( + + )} + + + {/* pipes */} + {Array.from({ length: 3 }).map((_, idx) => ( + + + el && pipeMaterialsRef.current.push(el)} + color="#a5aca8" + roughness={0.0} + metalness={0.4} + /> + + ))} + + {/* brackets */} + + {Array.from({ length: 5 }).map((_, idx) => { + const zOffset = -(idx * 8 + 4); + return ( + + + el && bracketMaterialsRef.current.push(el)} + color="#a5aca8" + roughness={0.0} + metalness={0.4} + /> + + ); + })} + + + ))} + + ); +} diff --git a/src/app/fear/scene-components/player.tsx b/src/app/fear/scene-components/player.tsx new file mode 100644 index 0000000..0bc3248 --- /dev/null +++ b/src/app/fear/scene-components/player.tsx @@ -0,0 +1,140 @@ +import { useFrame, useThree } from "@react-three/fiber"; +import { useEffect, useRef } from "react"; +import * as THREE from "three"; +import { FEAR_SETTINGS, fearState } from "../state"; +import { PointerLockControls } from "@react-three/drei"; + +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(null); + const movementCounter = useRef(0); + + const confirmedSegment = useRef(0); + const hasTriggeredThisSegment = useRef(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 = 5 + 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 ( + <> + + + + ); +} diff --git a/src/app/fear/state.ts b/src/app/fear/state.ts index 131410e..3d2fb42 100644 --- a/src/app/fear/state.ts +++ b/src/app/fear/state.ts @@ -5,6 +5,7 @@ export const FEAR_SETTINGS = { HALLWAY_WIDTH: 6, HALLWAY_HEIGHT: 5, PLAYER_HEIGHT: 3, + PLAYER_SPEED: 6, WALL_BUFFER: 0.6, }; @@ -14,6 +15,8 @@ export const fearState = { loopCount: 0, currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH, isRustActive: false, + finaleTriggered: false, + wasCaught: false, subscribe(listener: () => void) { listeners.add(listener); @@ -26,6 +29,7 @@ export const fearState = { update(delta: number) { this.isRustActive = this.loopCount >= 3; + this.finaleTriggered = this.loopCount >= 4; const targetWidth = this.loopCount >= 2 ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH; const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta); @@ -38,7 +42,11 @@ export const fearState = { registerLoop(direction: 'forward' | 'backward') { this.loopCount += 1; - console.log(`Hallway looped ${direction}. Total loops: ${this.loopCount}`); + this.emit(); + }, + + registerCaught() { + this.wasCaught = true; this.emit(); } }; \ No newline at end of file