import { useEffect, useRef, useState } from "react"; import { FEAR_SETTINGS, fearState } from "../state"; import { useTexture, PositionalAudio } from "@react-three/drei"; import * as THREE from "three"; import { useFrame } from "@react-three/fiber"; function ShaderPatch(shader: { vertexShader: string, fragmentShader: string, uniforms: Object }) { shader.vertexShader = ` varying float vDepth; #ifdef USE_MAP varying vec2 vAffineUv; #endif ${shader.vertexShader} `; shader.vertexShader = shader.vertexShader.replace( `#include `, ` vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); gl_Position = projectionMatrix * mvPosition; float precisionModifier = 200.0; gl_Position.xy /= gl_Position.w; gl_Position.xy = floor(gl_Position.xy * precisionModifier) / precisionModifier; gl_Position.xy *= gl_Position.w; vDepth = gl_Position.w; #ifdef USE_MAP vAffineUv = vMapUv * gl_Position.w; #endif ` ); shader.fragmentShader = ` varying float vDepth; #ifdef USE_MAP varying vec2 vAffineUv; #endif ${shader.fragmentShader} `; shader.fragmentShader = shader.fragmentShader.replace( `#include `, ` #ifdef USE_MAP vec2 flatAffineUV = vAffineUv / max(vDepth, 0.001); vec2 warpDiff = flatAffineUV - vMapUv; float warpDist = length(warpDiff); float maxDistortion = 0.25; float falloff = maxDistortion / (maxDistortion + warpDist); vec2 finalUV = vMapUv + (warpDiff * falloff); vec4 texelColor = texture2D( map, finalUV ); diffuseColor *= texelColor; #endif ` ); } interface DoorProps { position: [number, number, number]; rotation: [number, number, number]; } function Door({ position, rotation }: DoorProps) { const [soundUrl, setSoundUrl] = useState(null); const currentSound = useRef(null); const steelTex = useTexture('fear/img/steel.png'); useEffect(() => { const interval = setInterval(() => { if (Math.random() < 0.02) { const chosenSound = Math.random() < 0.5 ? "fear/snd/knock1.mp3" : "fear/snd/knock2.mp3"; setSoundUrl(chosenSound); currentSound.current = chosenSound; } }, 5000); return () => clearInterval(interval); }, []); const handleAudioEnded = () => { setSoundUrl(null); currentSound.current = null; }; return ( {/* frame */} {/* panel */} {/* handle */} {soundUrl && ( )} ); } export default function Hallway() { const [width, setWidth] = useState(fearState.currentWidth); const [floorTex, wallTex, rustWallTex, rustFloorTex] = useTexture([ 'fear/img/concrete-floor.png', 'fear/img/concrete-wall.png', 'fear/img/rust.png', 'fear/img/rust.png' ]); useEffect(() => { [floorTex, wallTex, rustWallTex, rustFloorTex].forEach((tex) => { tex.wrapS = tex.wrapT = THREE.RepeatWrapping; tex.minFilter = tex.magFilter = THREE.NearestFilter; tex.colorSpace = THREE.SRGBColorSpace; }); }, [floorTex, wallTex, rustWallTex, rustFloorTex]); 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; /* 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); rustWallTex.repeat.set(10, 1); rustFloorTex.repeat.set(horizontalTexRepeat, 10); floorTex.needsUpdate = true; wallTex.needsUpdate = true; rustWallTex.needsUpdate = true; rustFloorTex.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; 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, targetRustTex: THREE.Texture, activeColor: string, defaultColor: string, activeRough: number, defaultRough: number, activeMetal: number, defaultMetal: number) => { materials.forEach(mat => { if (!mat) return; const targetTex = isRustActive ? targetRustTex : 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, rustWallTex, "#c5c0be", "#ffffff", 0.95, 0.7, 0.05, 0.1); updateMaterials(floorMaterialsRef.current, floorTex, rustFloorTex, "#cabdb9", "#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} onBeforeCompile={ShaderPatch} /> {/* floor */} { if (el) floorMaterialsRef.current.push(el); }} map={floorTex} onBeforeCompile={ShaderPatch} /> {/* ceiling */} { if (el) floorMaterialsRef.current.push(el); }} map={floorTex} onBeforeCompile={ShaderPatch} /> {/* left wall */} { if (el) wallMaterialsRef.current.push(el); }} map={wallTex} onBeforeCompile={ShaderPatch} /> {!isRustActive && ( <> )} {/* right wall */} { if (el) wallMaterialsRef.current.push(el); }} map={wallTex} onBeforeCompile={ShaderPatch} /> {!isRustActive && ( )} {/* pipes */} {Array.from({ length: 3 }).map((_, idx) => ( el && pipeMaterialsRef.current.push(el)} color="#a5aca8" roughness={0.0} metalness={0.4} onBeforeCompile={ShaderPatch} /> ))} {/* 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} onBeforeCompile={ShaderPatch} /> ); })} ))} ); }