Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 339c660bcb | |||
| fe686b0071 |
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
+21
-472
@@ -2,489 +2,37 @@
|
|||||||
|
|
||||||
import './page.css';
|
import './page.css';
|
||||||
|
|
||||||
import { PointerLockControls, useTexture } from "@react-three/drei";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
import { BrightnessContrast, EffectComposer, HueSaturation, Noise, Pixelation, Vignette } from "@react-three/postprocessing";
|
||||||
import { BrightnessContrast, EffectComposer, Noise, Pixelation, Vignette } from "@react-three/postprocessing";
|
import { Suspense, useEffect, useState } from "react";
|
||||||
import { Suspense, useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
import * as THREE from "three";
|
|
||||||
import { AmbientSound } from './scene-components/ambient-sound';
|
import { AmbientSound } from './scene-components/ambient-sound';
|
||||||
|
|
||||||
import { fearState, FEAR_SETTINGS } from './state';
|
import { fearState } from './state';
|
||||||
|
|
||||||
function Player() {
|
import TheCreature from './scene-components/creature';
|
||||||
const { camera } = useThree();
|
import Player from './scene-components/player';
|
||||||
const controls = usePlayerControls();
|
import Hallway from './scene-components/hallway';
|
||||||
|
|
||||||
const flashlightRef = useRef<THREE.SpotLight>(null);
|
function PostProcessing() {
|
||||||
const movementCounter = useRef<number>(0);
|
const [getCaught, setCaught] = useState(fearState.wasCaught);
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<PointerLockControls />
|
|
||||||
<spotLight
|
|
||||||
ref={flashlightRef}
|
|
||||||
distance={22}
|
|
||||||
angle={0.35}
|
|
||||||
penumbra={0.7}
|
|
||||||
intensity={5}
|
|
||||||
color="#fffaed"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<group position={position} rotation={rotation}>
|
|
||||||
<mesh position={[0, 2, -0.14]}>
|
|
||||||
<boxGeometry args={[2.4, 4.0, 0.2]} />
|
|
||||||
<meshStandardMaterial color="#8a8585" roughness={0.8} metalness={0.2} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
<mesh position={[0, 1.95, -0.08]}>
|
|
||||||
<boxGeometry args={[2.1, 3.8, 0.1]} />
|
|
||||||
<meshStandardMaterial color="#4e4b4b" roughness={0.7} metalness={0.2} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
<mesh position={[0.9, 1.8, 0.08]}>
|
|
||||||
<boxGeometry args={[0.08, 0.08, 0.15]} />
|
|
||||||
<meshStandardMaterial color="#4e4b4b" roughness={0.4} metalness={0.2} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<THREE.PointLight>(null);
|
|
||||||
const lightRef2 = useRef<THREE.PointLight>(null);
|
|
||||||
const matRef1 = useRef<THREE.MeshStandardMaterial>(null);
|
|
||||||
const matRef2 = 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(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = fearState.subscribe(() => {
|
const unsubscribe = fearState.subscribe(() => {
|
||||||
setWidth(fearState.currentWidth);
|
setCaught(fearState.wasCaught);
|
||||||
setIsRustActive(fearState.isRustActive);
|
|
||||||
});
|
});
|
||||||
return () => unsubscribe();
|
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 (
|
|
||||||
<>
|
|
||||||
<ambientLight intensity={0.0225} />
|
|
||||||
|
|
||||||
{segments.map((segmentValue, index) => (
|
|
||||||
/*
|
|
||||||
lights
|
|
||||||
*/
|
|
||||||
<group
|
|
||||||
key={segmentValue}
|
|
||||||
ref={(el) => { if (el) segmentsRef.current[index] = el; }}
|
|
||||||
position={[0, 0, segmentValue * FEAR_SETTINGS.HALLWAY_LENGTH]}
|
|
||||||
>
|
|
||||||
<group position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT - 0.1, -FEAR_SETTINGS.HALLWAY_LENGTH / 4]}>
|
|
||||||
<pointLight
|
|
||||||
ref={segmentValue === 0 ? lightRef1 : null}
|
|
||||||
intensity={0.9}
|
|
||||||
distance={15}
|
|
||||||
color="#a8a1a1"
|
|
||||||
/>
|
|
||||||
<mesh position={[0, 0.09, 0]}>
|
|
||||||
<boxGeometry args={[0.3, 0.01, 0.3]} />
|
|
||||||
<meshStandardMaterial
|
|
||||||
ref={segmentValue === 0 ? matRef1 : null}
|
|
||||||
color="#111111"
|
|
||||||
emissive="#a8a1a1"
|
|
||||||
emissiveIntensity={segmentValue === 0 ? 0 : 0.8}
|
|
||||||
roughness={0.9}
|
|
||||||
/>
|
|
||||||
</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]} />
|
|
||||||
<meshStandardMaterial
|
|
||||||
ref={(el) => el && floorMaterialsRef.current.push(el)}
|
|
||||||
map={floorTex}
|
|
||||||
roughness={0.8}
|
|
||||||
metalness={0.2}
|
|
||||||
/>
|
|
||||||
</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]} />
|
|
||||||
<meshStandardMaterial
|
|
||||||
ref={(el) => el && floorMaterialsRef.current.push(el)}
|
|
||||||
map={floorTex}
|
|
||||||
roughness={0.8}
|
|
||||||
metalness={0.2}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
{/* l 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]} />
|
|
||||||
<meshStandardMaterial
|
|
||||||
ref={(el) => el && wallMaterialsRef.current.push(el)}
|
|
||||||
map={wallTex}
|
|
||||||
roughness={0.7}
|
|
||||||
metalness={0.1}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* r 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]} />
|
|
||||||
<meshStandardMaterial
|
|
||||||
ref={(el) => el && wallMaterialsRef.current.push(el)}
|
|
||||||
map={wallTex}
|
|
||||||
roughness={0.7}
|
|
||||||
metalness={0.1}
|
|
||||||
/>
|
|
||||||
</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}
|
|
||||||
/>
|
|
||||||
</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}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PostProcessing() {
|
|
||||||
return (<EffectComposer>
|
return (<EffectComposer>
|
||||||
<Pixelation granularity={12} />
|
<Pixelation granularity={getCaught ? 18 : 12} />
|
||||||
<Vignette />
|
<Vignette />
|
||||||
<Noise opacity={0.005} />
|
<Noise opacity={getCaught ? 0.01 : 0.005} />
|
||||||
<BrightnessContrast
|
<BrightnessContrast
|
||||||
brightness={-0.01}
|
brightness={-0.01}
|
||||||
contrast={0.05}
|
contrast={0.05}
|
||||||
/>
|
/>
|
||||||
|
<HueSaturation saturation={getCaught ? 1 : 0} />
|
||||||
</EffectComposer>)
|
</EffectComposer>)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,16 +57,17 @@ export default function Fear() {
|
|||||||
|
|
||||||
<fogExp2 attach='fog' args={[0x050505, 0.035]} />
|
<fogExp2 attach='fog' args={[0x050505, 0.035]} />
|
||||||
<PostProcessing />
|
<PostProcessing />
|
||||||
|
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Hallway />
|
<Hallway />
|
||||||
|
<TheCreature />
|
||||||
|
<Player />
|
||||||
|
|
||||||
|
<AmbientSound
|
||||||
|
url='fear/snd/ambience.mp3'
|
||||||
|
volume={isRustActive ? 0 : 1}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<AmbientSound
|
|
||||||
url='fear/snd/ambience.mp3'
|
|
||||||
volume={isRustActive ? 0 : 1}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Player />
|
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
@@ -11,31 +11,41 @@ export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audio = new Audio(url)
|
const audio = new Audio(url)
|
||||||
audio.loop = true
|
audio.loop = true
|
||||||
audio.volume = volume
|
|
||||||
audioRef.current = audio
|
audioRef.current = audio
|
||||||
|
|
||||||
const startAudio = () => {
|
|
||||||
audio.play().catch((err) => {
|
|
||||||
console.warn('Autoplay blocked. Waiting for user interaction.', err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
startAudio()
|
|
||||||
|
|
||||||
window.addEventListener('click', startAudio, { once: true })
|
|
||||||
window.addEventListener('keydown', startAudio, { once: true })
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('click', startAudio)
|
|
||||||
window.removeEventListener('keydown', startAudio)
|
|
||||||
audio.pause()
|
audio.pause()
|
||||||
|
audio.src = ''
|
||||||
audioRef.current = null
|
audioRef.current = null
|
||||||
}
|
}
|
||||||
}, [url])
|
}, [url])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (audioRef.current) {
|
const audio = audioRef.current
|
||||||
audioRef.current.volume = volume
|
if (!audio) return
|
||||||
|
|
||||||
|
audio.volume = volume
|
||||||
|
|
||||||
|
const startAudio = () => {
|
||||||
|
if (audio.volume > 0) {
|
||||||
|
audio.play().catch((err) => {
|
||||||
|
console.warn('Autoplay blocked. Waiting for user interaction.', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (volume === 0) {
|
||||||
|
audio.pause()
|
||||||
|
} else {
|
||||||
|
startAudio()
|
||||||
|
|
||||||
|
window.addEventListener('click', startAudio, { once: true })
|
||||||
|
window.addEventListener('keydown', startAudio, { once: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', startAudio)
|
||||||
|
window.removeEventListener('keydown', startAudio)
|
||||||
}
|
}
|
||||||
}, [volume])
|
}, [volume])
|
||||||
|
|
||||||
|
|||||||
@@ -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<THREE.Mesh>(null);
|
||||||
|
const { camera } = useThree();
|
||||||
|
|
||||||
|
const [hasTriggered, setHasTriggered] = useState(false);
|
||||||
|
const [isSpawned, setIsSpawned] = useState(false);
|
||||||
|
|
||||||
|
const speed = 15;
|
||||||
|
const globalDistance = useRef<number>(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 (
|
||||||
|
<mesh
|
||||||
|
ref={meshRef}
|
||||||
|
visible={currentLoop >= 4}
|
||||||
|
>
|
||||||
|
<planeGeometry args={[3.0, 4.8]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
map={texture}
|
||||||
|
transparent={true}
|
||||||
|
depthWrite={false}
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<group position={position} rotation={rotation}>
|
||||||
|
<mesh position={[0, 2, -0.14]}>
|
||||||
|
<boxGeometry args={[2.4, 4.0, 0.2]} />
|
||||||
|
<meshStandardMaterial color="#8a8585" roughness={0.8} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<mesh position={[0, 1.95, -0.08]}>
|
||||||
|
<boxGeometry args={[2.1, 3.8, 0.1]} />
|
||||||
|
<meshStandardMaterial color="#4e4b4b" roughness={0.7} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<mesh position={[0.9, 1.8, 0.08]}>
|
||||||
|
<boxGeometry args={[0.08, 0.08, 0.15]} />
|
||||||
|
<meshStandardMaterial color="#4e4b4b" roughness={0.4} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<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;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<ambientLight intensity={0.0225} />
|
||||||
|
|
||||||
|
{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}
|
||||||
|
/>
|
||||||
|
</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]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
|
||||||
|
map={floorTex}
|
||||||
|
roughness={0.8}
|
||||||
|
metalness={0.2}
|
||||||
|
/>
|
||||||
|
</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]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
|
||||||
|
map={floorTex}
|
||||||
|
roughness={0.8}
|
||||||
|
metalness={0.2}
|
||||||
|
/>
|
||||||
|
</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]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
|
||||||
|
map={wallTex}
|
||||||
|
roughness={0.7}
|
||||||
|
metalness={0.1}
|
||||||
|
/>
|
||||||
|
</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]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
|
||||||
|
map={wallTex}
|
||||||
|
roughness={0.7}
|
||||||
|
metalness={0.1}
|
||||||
|
/>
|
||||||
|
</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}
|
||||||
|
/>
|
||||||
|
</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}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<THREE.SpotLight>(null);
|
||||||
|
const movementCounter = useRef<number>(0);
|
||||||
|
|
||||||
|
const confirmedSegment = useRef<number>(0);
|
||||||
|
const hasTriggeredThisSegment = useRef<boolean>(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 (
|
||||||
|
<>
|
||||||
|
<PointerLockControls />
|
||||||
|
<spotLight
|
||||||
|
ref={flashlightRef}
|
||||||
|
distance={22}
|
||||||
|
angle={0.35}
|
||||||
|
penumbra={0.7}
|
||||||
|
intensity={5}
|
||||||
|
color="#fffaed"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ export const FEAR_SETTINGS = {
|
|||||||
HALLWAY_WIDTH: 6,
|
HALLWAY_WIDTH: 6,
|
||||||
HALLWAY_HEIGHT: 5,
|
HALLWAY_HEIGHT: 5,
|
||||||
PLAYER_HEIGHT: 3,
|
PLAYER_HEIGHT: 3,
|
||||||
|
PLAYER_SPEED: 6,
|
||||||
WALL_BUFFER: 0.6,
|
WALL_BUFFER: 0.6,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ export const fearState = {
|
|||||||
loopCount: 0,
|
loopCount: 0,
|
||||||
currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH,
|
currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH,
|
||||||
isRustActive: false,
|
isRustActive: false,
|
||||||
|
finaleTriggered: false,
|
||||||
|
wasCaught: false,
|
||||||
|
|
||||||
subscribe(listener: () => void) {
|
subscribe(listener: () => void) {
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
@@ -26,6 +29,7 @@ export const fearState = {
|
|||||||
|
|
||||||
update(delta: number) {
|
update(delta: number) {
|
||||||
this.isRustActive = this.loopCount >= 3;
|
this.isRustActive = this.loopCount >= 3;
|
||||||
|
this.finaleTriggered = this.loopCount >= 4;
|
||||||
|
|
||||||
const targetWidth = this.loopCount >= 2 ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH;
|
const targetWidth = this.loopCount >= 2 ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH;
|
||||||
const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta);
|
const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta);
|
||||||
@@ -38,7 +42,11 @@ export const fearState = {
|
|||||||
|
|
||||||
registerLoop(direction: 'forward' | 'backward') {
|
registerLoop(direction: 'forward' | 'backward') {
|
||||||
this.loopCount += 1;
|
this.loopCount += 1;
|
||||||
console.log(`Hallway looped ${direction}. Total loops: ${this.loopCount}`);
|
this.emit();
|
||||||
|
},
|
||||||
|
|
||||||
|
registerCaught() {
|
||||||
|
this.wasCaught = true;
|
||||||
this.emit();
|
this.emit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user