diff --git a/src/app/fear/page.tsx b/src/app/fear/page.tsx index 33ee720..5a61996 100644 --- a/src/app/fear/page.tsx +++ b/src/app/fear/page.tsx @@ -5,17 +5,12 @@ 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 } from "react"; +import { Suspense, useEffect, useRef, useState } from "react"; import * as THREE from "three"; import { AmbientSound } from './scene-components/ambient-sound'; -import { MeshStandardNodeMaterial } from 'three/webgpu'; -const HALLWAY_LENGTH = 40; -const HALLWAY_WIDTH = 6; -const HALLWAY_HEIGHT = 5; - -const PLAYER_HEIGHT = 3; +import { fearState, FEAR_SETTINGS } from './state'; function Player() { const { camera } = useThree(); @@ -24,21 +19,27 @@ function Player() { const flashlightRef = useRef(null); const movementCounter = useRef(0); - const frontVector = new THREE.Vector3(); - const sideVector = new THREE.Vector3(); + const forward = new THREE.Vector3(); + const side = new THREE.Vector3(); const direction = new THREE.Vector3(); - const viewDirection = new THREE.Vector3(); useFrame((state, delta) => { - frontVector.set(0, 0, Number(controls.Backward) - Number(controls.Forward)); - sideVector.set(Number(controls.Left) - Number(controls.Right), 0, 0); + camera.getWorldDirection(forward); + forward.y = 0; + forward.normalize(); - direction - .subVectors(frontVector, sideVector) - .normalize() - .multiplyScalar(4 * delta) - .applyEuler(camera.rotation); + 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; @@ -46,33 +47,42 @@ function Player() { const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right; if (isMoving) { movementCounter.current += delta * 10; - camera.position.y = PLAYER_HEIGHT + Math.sin(movementCounter.current) * 0.08; + camera.position.y = FEAR_SETTINGS.PLAYER_HEIGHT + Math.sin(movementCounter.current) * 0.08; camera.position.x += Math.cos(movementCounter.current / 2) * 0.006; - } else - camera.position.y = THREE.MathUtils.lerp(camera.position.y, PLAYER_HEIGHT, 5 * delta); - + } 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); - flashlightRef.current.target.position + 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 wallBuffer = 0.6; - const HALLWAY_WIDTH = 6; - const HALLWAY_LENGTH = 40; + const minX = -fearState.currentWidth / 2 + FEAR_SETTINGS.WALL_BUFFER; + const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER; - if (camera.position.x < -HALLWAY_WIDTH / 2 + wallBuffer) camera.position.x = -HALLWAY_WIDTH / 2 + wallBuffer; - if (camera.position.x > HALLWAY_WIDTH / 2 - wallBuffer) camera.position.x = HALLWAY_WIDTH / 2 - wallBuffer; + if (camera.position.x < minX) camera.position.x = minX; + if (camera.position.x > maxX) camera.position.x = maxX; - if (camera.position.z < -HALLWAY_LENGTH) camera.position.z += HALLWAY_LENGTH; - if (camera.position.z > 0) camera.position.z -= HALLWAY_LENGTH; + 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 ( @@ -146,110 +156,320 @@ function Door({ position, rotation }: DoorProps) { } function Hallway() { - const [floorTex, wallTex] = useTexture([ - 'fear/img/concrete-floor.jpg', - 'fear/img/concrete-wall.jpg' + 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.wrapS = floorTex.wrapT = THREE.RepeatWrapping; - floorTex.repeat.set(1, 10); - floorTex.minFilter = THREE.NearestFilter; - floorTex.magFilter = THREE.NearestFilter; - - - wallTex.wrapS = wallTex.wrapT = THREE.RepeatWrapping; - wallTex.repeat.set(10, 1); - wallTex.minFilter = THREE.NearestFilter; - wallTex.magFilter = THREE.NearestFilter; + [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); - useFrame((state) => { + 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; - const flicker = Math.sin(time * 10) * Math.cos(time * 3) > 0.4 ? 0 : 0.8; - if (lightRef1.current) lightRef1.current.intensity = flicker; - if (lightRef2.current) lightRef2.current.intensity = flicker * 0.5; + + 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((i) => ( - - - + {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} + /> - {/* roof */} - - - + {/* 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 */} - - - - - - {/* doors */} - - - - - + + + + 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} + /> ))} - {Array.from({ length: 5 }).map((_, idx) => { - const zOffset = -(idx * 8 + 4); - - return ( - - - - - ); - })} + {/* 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} + /> + + ); + })} + ))} @@ -258,7 +478,7 @@ function Hallway() { function PostProcessing() { return ( - + { + const unsubscribe = fearState.subscribe(() => { + setIsRustActive(fearState.isRustActive); + }); + return () => unsubscribe(); + }, []); + return (<> - + diff --git a/src/app/fear/state.ts b/src/app/fear/state.ts new file mode 100644 index 0000000..131410e --- /dev/null +++ b/src/app/fear/state.ts @@ -0,0 +1,44 @@ +import * as THREE from 'three'; + +export const FEAR_SETTINGS = { + HALLWAY_LENGTH: 40, + HALLWAY_WIDTH: 6, + HALLWAY_HEIGHT: 5, + PLAYER_HEIGHT: 3, + WALL_BUFFER: 0.6, +}; + +const listeners = new Set<() => void>(); + +export const fearState = { + loopCount: 0, + currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH, + isRustActive: false, + + subscribe(listener: () => void) { + listeners.add(listener); + return () => { listeners.delete(listener); }; + }, + + emit() { + listeners.forEach((listener) => listener()); + }, + + update(delta: number) { + this.isRustActive = this.loopCount >= 3; + + const targetWidth = this.loopCount >= 2 ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH; + const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta); + + if (Math.abs(this.currentWidth - newWidth) > 0.001) { + this.currentWidth = newWidth; + this.emit(); + } + }, + + registerLoop(direction: 'forward' | 'backward') { + this.loopCount += 1; + console.log(`Hallway looped ${direction}. Total loops: ${this.loopCount}`); + this.emit(); + } +}; \ No newline at end of file