459 lines
18 KiB
TypeScript
459 lines
18 KiB
TypeScript
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 <project_vertex>`,
|
|
`
|
|
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 <map_fragment>`,
|
|
`
|
|
#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<string | null>(null);
|
|
const currentSound = useRef<string | null>(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 (
|
|
<group position={position} rotation={rotation}>
|
|
{/* frame */}
|
|
<mesh position={[0, 2, -0.1]}>
|
|
<boxGeometry args={[2.4, 4.0, 0.2, 4, 4, 1]} />
|
|
<meshStandardMaterial map={steelTex} color="#8d8d8d" onBeforeCompile={ShaderPatch} />
|
|
</mesh>
|
|
|
|
{/* panel */}
|
|
<mesh position={[0, 1.95, -0.0]}>
|
|
<boxGeometry args={[2.1, 3.8, 0.1, 4, 4, 1]} />
|
|
<meshStandardMaterial map={steelTex} color="#4e4a4a" onBeforeCompile={ShaderPatch} />
|
|
</mesh>
|
|
|
|
{/* handle */}
|
|
<mesh position={[0.75, 1.8, .085]}>
|
|
<boxGeometry args={[0.3, 0.08, 0.1]} />
|
|
<meshStandardMaterial map={steelTex} color="#ffffff" onBeforeCompile={ShaderPatch} />
|
|
</mesh>
|
|
|
|
{soundUrl && (
|
|
<PositionalAudio
|
|
url={soundUrl}
|
|
distance={25}
|
|
loop={false}
|
|
autoplay={true}
|
|
onEnded={handleAudioEnded}
|
|
/>
|
|
)}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
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<number>(0);
|
|
const nextEventTime = useRef<number>(5);
|
|
|
|
const segmentsRef = useRef<THREE.Group[]>([]);
|
|
const wallMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
|
|
const floorMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
|
|
const pipeMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
|
|
const bracketMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
|
|
|
|
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) => (
|
|
<group
|
|
key={poolIndex}
|
|
ref={(el) => { if (el) segmentsRef.current[poolIndex] = el; }}
|
|
position={[0, 0, 0]}
|
|
>
|
|
{/* lights */}
|
|
<group position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT - 0.1, -FEAR_SETTINGS.HALLWAY_LENGTH / 4]}>
|
|
<pointLight
|
|
ref={(el) => { lightRefs.current[poolIndex] = el; }}
|
|
intensity={0.9}
|
|
distance={15}
|
|
color="#a8a1a1"
|
|
/>
|
|
<mesh position={[0, 0.09, 0]}>
|
|
<boxGeometry args={[0.3, 0.01, 0.3]} />
|
|
<meshStandardMaterial
|
|
ref={(el) => { matRefs.current[poolIndex] = el; }}
|
|
color="#111111"
|
|
emissive="#a8a1a1"
|
|
emissiveIntensity={0.8}
|
|
roughness={0.9}
|
|
onBeforeCompile={ShaderPatch}
|
|
/>
|
|
</mesh>
|
|
</group>
|
|
|
|
{/* floor */}
|
|
<mesh
|
|
name="floor-mesh"
|
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
position={[0, 0, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
|
|
>
|
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH, 4, 10]} />
|
|
<meshStandardMaterial
|
|
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
|
|
map={floorTex}
|
|
onBeforeCompile={ShaderPatch}
|
|
/>
|
|
</mesh>
|
|
|
|
{/* ceiling */}
|
|
<mesh
|
|
name="ceiling-mesh"
|
|
rotation={[Math.PI / 2, 0, 0]}
|
|
position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
|
|
>
|
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH, 4, 10]} />
|
|
<meshStandardMaterial
|
|
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
|
|
map={floorTex}
|
|
onBeforeCompile={ShaderPatch}
|
|
/>
|
|
</mesh>
|
|
|
|
{/* left wall */}
|
|
<group name="left-wall-group">
|
|
<mesh rotation={[0, Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}>
|
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT, 10, 4]} />
|
|
<meshStandardMaterial
|
|
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
|
|
map={wallTex}
|
|
onBeforeCompile={ShaderPatch}
|
|
/>
|
|
</mesh>
|
|
{!isRustActive && (
|
|
<>
|
|
<Door position={[0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.25]} rotation={[0, Math.PI / 2, 0]} />
|
|
<Door position={[0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.85]} rotation={[0, Math.PI / 2, 0]} />
|
|
</>
|
|
)}
|
|
</group>
|
|
|
|
{/* right wall */}
|
|
<group name="right-wall-group">
|
|
<mesh rotation={[0, -Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}>
|
|
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT, 10, 4]} />
|
|
<meshStandardMaterial
|
|
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
|
|
map={wallTex}
|
|
onBeforeCompile={ShaderPatch}
|
|
/>
|
|
</mesh>
|
|
{!isRustActive && (
|
|
<Door position={[-0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.65]} rotation={[0, -Math.PI / 2, 0]} />
|
|
)}
|
|
</group>
|
|
|
|
{/* pipes */}
|
|
{Array.from({ length: 3 }).map((_, idx) => (
|
|
<mesh
|
|
key={idx}
|
|
name={`pipe-${idx}`}
|
|
rotation={[Math.PI / 2, 0, 0]}
|
|
position={[-FEAR_SETTINGS.HALLWAY_WIDTH / 2 + 0.4 + (idx * 0.20), FEAR_SETTINGS.HALLWAY_HEIGHT - 0.2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
|
|
>
|
|
<cylinderGeometry args={[0.06, 0.06, FEAR_SETTINGS.HALLWAY_LENGTH, 4]} />
|
|
<meshStandardMaterial
|
|
ref={(el) => el && pipeMaterialsRef.current.push(el)}
|
|
color="#a5aca8"
|
|
roughness={0.0}
|
|
metalness={0.4}
|
|
onBeforeCompile={ShaderPatch}
|
|
/>
|
|
</mesh>
|
|
))}
|
|
|
|
{/* brackets */}
|
|
<group name="brackets-group">
|
|
{Array.from({ length: 5 }).map((_, idx) => {
|
|
const zOffset = -(idx * 8 + 4);
|
|
return (
|
|
<mesh
|
|
key={`bracket-${idx}`}
|
|
position={[-FEAR_SETTINGS.HALLWAY_WIDTH / 2 + 0.6, FEAR_SETTINGS.HALLWAY_HEIGHT - 0.15, zOffset]}
|
|
>
|
|
<boxGeometry args={[0.7, 0.3, 0.15]} />
|
|
<meshStandardMaterial
|
|
ref={(el) => el && bracketMaterialsRef.current.push(el)}
|
|
color="#a5aca8"
|
|
roughness={0.0}
|
|
metalness={0.4}
|
|
onBeforeCompile={ShaderPatch}
|
|
/>
|
|
</mesh>
|
|
);
|
|
})}
|
|
</group>
|
|
</group>
|
|
))}
|
|
</>
|
|
);
|
|
}
|