style: run format:apply

This commit is contained in:
2026-06-26 08:48:31 -03:00
parent ab3bf047d4
commit eebd87a650
21 changed files with 1856 additions and 1484 deletions
+1 -1
View File
@@ -7,4 +7,4 @@
outline: none;
user-select: none;
touch-action: none;
}
}
+95 -79
View File
@@ -2,9 +2,16 @@
import './page.css';
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { BrightnessContrast, EffectComposer, HueSaturation, Noise, Pixelation, Vignette } from "@react-three/postprocessing";
import { Suspense, useEffect, useState } from "react";
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import {
BrightnessContrast,
EffectComposer,
HueSaturation,
Noise,
Pixelation,
Vignette
} from '@react-three/postprocessing';
import { Suspense, useEffect, useState } from 'react';
import { AmbientSound } from './scene-components/ambient-sound';
@@ -18,103 +25,112 @@ import { AudioListener } from 'three';
import FinaleText from './scene-components/finale-text';
function PostProcessing() {
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWasCaught(fearState.wasCaught);
});
return () => unsubscribe();
}, []);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWasCaught(fearState.wasCaught);
});
return () => unsubscribe();
}, []);
return (<EffectComposer>
<Pixelation granularity={wasCaught ? 18 : 10} />
<Vignette />
<Noise opacity={wasCaught ? 0.01 : 0.003} />
<BrightnessContrast
brightness={-0.01}
contrast={0.05}
/>
<HueSaturation saturation={wasCaught ? 1 : 0} />
</EffectComposer>)
return (
<EffectComposer>
<Pixelation granularity={wasCaught ? 18 : 10} />
<Vignette />
<Noise opacity={wasCaught ? 0.01 : 0.003} />
<BrightnessContrast brightness={-0.01} contrast={0.05} />
<HueSaturation saturation={wasCaught ? 1 : 0} />
</EffectComposer>
);
}
function ListenerCreator() {
const { camera } = useThree();
const { camera } = useThree();
useEffect(() => {
const listener = new AudioListener();
camera.add(listener);
useEffect(() => {
const listener = new AudioListener();
camera.add(listener);
return () => {
camera.remove(listener);
};
}, [camera]);
return () => {
camera.remove(listener);
};
}, [camera]);
return null;
return null;
}
function FearStateUpdater() {
useFrame((state, delta) => {
fearState.update(delta);
});
return null;
useFrame((state, delta) => {
fearState.update(delta);
});
return null;
}
export default function Fear() {
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
const [wasCaught, setWasCaught] = useState(fearState.isRustActive);
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
const [wasCaught, setWasCaught] = useState(fearState.isRustActive);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setIsRustActive(fearState.isRustActive);
setWasCaught(fearState.wasCaught)
});
return () => unsubscribe();
}, []);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setIsRustActive(fearState.isRustActive);
setWasCaught(fearState.wasCaught);
});
return () => unsubscribe();
}, []);
return (<>
<Canvas
shadows
gl={{ antialias: true }}
className='canvas'
camera={{ position: [0, 3, -5], fov: 55, far: 100 }}
>
<FearStateUpdater />
return (
<>
<Canvas
shadows
gl={{ antialias: true }}
className='canvas'
camera={{ position: [0, 3, -5], fov: 55, far: 100 }}
>
<FearStateUpdater />
<ListenerCreator />
<ListenerCreator />
<color attach="background" args={['#050505']} />
<color attach='background' args={['#050505']} />
{FEAR_SETTINGS.TEST_MODE ? <ambientLight intensity={2} /> : <ambientLight intensity={0.0225} />}
{FEAR_SETTINGS.TEST_MODE ? null : <fogExp2 attach='fog' args={[0x050505, 0.035]} />}
{FEAR_SETTINGS.TEST_MODE ? null : < PostProcessing />}
{FEAR_SETTINGS.TEST_MODE ? (
<ambientLight intensity={2} />
) : (
<ambientLight intensity={0.0225} />
)}
{FEAR_SETTINGS.TEST_MODE ? null : (
<fogExp2 attach='fog' args={[0x050505, 0.035]} />
)}
{FEAR_SETTINGS.TEST_MODE ? null : <PostProcessing />}
<Suspense fallback={null}>
<Hallway />
<TheCreature />
<Player />
</Suspense>
<Suspense fallback={null}>
<Hallway />
<TheCreature />
<Player />
</Suspense>
<AmbientSound
key="ambient-1"
url='fear/snd/ambience.mp3'
volume={isRustActive ? 0 : 0.5}
/>
<AmbientSound
key='ambient-1'
url='fear/snd/ambience.mp3'
volume={isRustActive ? 0 : 0.5}
/>
<AmbientSound
key="ambient-2"
url='fear/snd/ambience2.mp3'
volume={isRustActive ? 1 : 0}
/>
<AmbientSound
key='ambient-2'
url='fear/snd/ambience2.mp3'
volume={isRustActive ? 1 : 0}
/>
{wasCaught ? <AmbientSound
key="ambient-glitch"
url='fear/snd/glitch.mp3'
volume={1}
/> : null}
</Canvas>
{wasCaught ? (
<AmbientSound
key='ambient-glitch'
url='fear/snd/glitch.mp3'
volume={1}
/>
) : null}
</Canvas>
<FinaleText />
</>)
}
<FinaleText />
</>
);
}
+51 -48
View File
@@ -1,64 +1,67 @@
import { useEffect, useRef } from 'react'
import { useEffect, useRef } from 'react';
interface AmbientSoundProps {
url: string
volume?: number
url: string;
volume?: number;
}
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
const audioRef = useRef<HTMLAudioElement | null>(null)
const targetVolumeRef = useRef<number>(volume)
const audioRef = useRef<HTMLAudioElement | null>(null);
const targetVolumeRef = useRef<number>(volume);
targetVolumeRef.current = volume
targetVolumeRef.current = volume;
useEffect(() => {
const audio = new Audio(url)
audio.loop = true
audio.volume = 0
audioRef.current = audio
useEffect(() => {
const audio = new Audio(url);
audio.loop = true;
audio.volume = 0;
audioRef.current = audio;
let componentsMounted = true
let componentsMounted = true;
const attemptPlay = () => {
if (!audioRef.current || !componentsMounted) return
audio.volume = targetVolumeRef.current
const attemptPlay = () => {
if (!audioRef.current || !componentsMounted) return;
if (audio.volume > 0 && audio.paused) {
audio.play().catch((err) => {
console.warn('Autoplay management holding clip playback execution.', err)
})
}
}
audio.volume = targetVolumeRef.current;
attemptPlay()
if (audio.volume > 0 && audio.paused) {
audio.play().catch((err) => {
console.warn(
'Autoplay management holding clip playback execution.',
err
);
});
}
};
window.addEventListener('click', attemptPlay)
window.addEventListener('keydown', attemptPlay)
attemptPlay();
return () => {
componentsMounted = false
window.removeEventListener('click', attemptPlay)
window.removeEventListener('keydown', attemptPlay)
audio.pause()
audio.src = ''
audioRef.current = null
}
}, [url])
window.addEventListener('click', attemptPlay);
window.addEventListener('keydown', attemptPlay);
useEffect(() => {
const audio = audioRef.current
if (!audio) return
return () => {
componentsMounted = false;
window.removeEventListener('click', attemptPlay);
window.removeEventListener('keydown', attemptPlay);
audio.pause();
audio.src = '';
audioRef.current = null;
};
}, [url]);
if (volume === 0) {
if (!audio.paused) audio.pause()
} else {
audio.volume = volume
if (audio.paused) {
audio.play().catch(() => {})
}
}
}, [volume])
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
return null
}
if (volume === 0) {
if (!audio.paused) audio.pause();
} else {
audio.volume = volume;
if (audio.paused) {
audio.play().catch(() => {});
}
}
}, [volume]);
return null;
}
+158 -158
View File
@@ -1,196 +1,196 @@
import { useTexture, PositionalAudio } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import { useEffect, useMemo, useRef, useState } from "react";
import { useTexture, PositionalAudio } from '@react-three/drei';
import { useFrame, useThree } from '@react-three/fiber';
import { useEffect, useMemo, useRef, useState } from 'react';
import * as THREE from "three";
import { FEAR_SETTINGS, fearState } from "../state";
import { ShaderPatch } from "../shader-patch";
import * as THREE from 'three';
import { FEAR_SETTINGS, fearState } from '../state';
import { ShaderPatch } from '../shader-patch';
useTexture.preload('fear/img/creature.png');
export default function TheCreature() {
const baseTexture = useTexture('fear/img/creature.png');
const baseTexture = useTexture('fear/img/creature.png');
const texture = useMemo(() => {
const t = baseTexture.clone();
t.needsUpdate = true;
return t;
}, [baseTexture]);
const texture = useMemo(() => {
const t = baseTexture.clone();
t.needsUpdate = true;
return t;
}, [baseTexture]);
const meshRef = useRef<THREE.Mesh>(null);
const audioRef = useRef<THREE.PositionalAudio>(null);
const { camera } = useThree();
const meshRef = useRef<THREE.Mesh>(null);
const audioRef = useRef<THREE.PositionalAudio>(null);
const { camera } = useThree();
const [hasTriggered, setHasTriggered] = useState(false);
const [isSpawned, setIsSpawned] = useState(false);
const [hasTriggered, setHasTriggered] = useState(false);
const [isSpawned, setIsSpawned] = useState(false);
const globalDistance = useRef<number>(32);
const [finaleTriggered, setFinaleTriggered] = useState(fearState.finaleTriggered);
const globalDistance = useRef<number>(32);
const [finaleTriggered, setFinaleTriggered] = useState(
fearState.finaleTriggered
);
const audioPlaying = useRef<boolean>(false);
const audioPlaying = useRef<boolean>(false);
const movePhase = useRef<'frozen' | 'lurching'>('frozen');
const phaseTimer = useRef<number>(1.5);
const movePhase = useRef<'frozen' | 'lurching'>('frozen');
const phaseTimer = useRef<number>(1.5);
const glitchCooldown = useRef<number>(0);
const isGlitchSpiking = useRef<boolean>(false);
const flickerCooldown = useRef<number>(0);
const glitchCooldown = useRef<number>(0);
const isGlitchSpiking = useRef<boolean>(false);
const flickerCooldown = useRef<number>(0);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setFinaleTriggered(fearState.finaleTriggered);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setFinaleTriggered(fearState.finaleTriggered);
if (!fearState.finaleTriggered) {
setIsSpawned(false);
setHasTriggered(false);
globalDistance.current = 32;
audioPlaying.current = false;
movePhase.current = 'frozen';
phaseTimer.current = 1.5;
if (!fearState.finaleTriggered) {
setIsSpawned(false);
setHasTriggered(false);
globalDistance.current = 32;
audioPlaying.current = false;
movePhase.current = 'frozen';
phaseTimer.current = 1.5;
if (audioRef.current && audioRef.current.isPlaying)
audioRef.current.stop();
}
});
return () => unsubscribe();
}, []);
if (audioRef.current && audioRef.current.isPlaying)
audioRef.current.stop();
}
});
return () => unsubscribe();
}, []);
useFrame((state, delta) => {
if (!fearState.finaleTriggered) return;
useFrame((state, delta) => {
if (!fearState.finaleTriggered) return;
const creature = meshRef.current;
if (!creature) return;
const creature = meshRef.current;
if (!creature) return;
if (!isSpawned) {
setIsSpawned(true);
globalDistance.current = 32;
movePhase.current = 'frozen';
phaseTimer.current = 1.0 + Math.random() * 1.5;
}
if (!isSpawned) {
setIsSpawned(true);
globalDistance.current = 32;
movePhase.current = 'frozen';
phaseTimer.current = 1.0 + Math.random() * 1.5;
}
if (!hasTriggered) {
if (globalDistance.current < 40)
setHasTriggered(true);
if (!hasTriggered) {
if (globalDistance.current < 40) setHasTriggered(true);
}
}
if (hasTriggered) {
phaseTimer.current -= delta;
if (hasTriggered) {
phaseTimer.current -= delta;
if (phaseTimer.current <= 0) {
if (movePhase.current === 'frozen') {
movePhase.current = 'lurching';
phaseTimer.current = 0.05 + Math.random() * 0.2;
} else {
movePhase.current = 'frozen';
const proximityFactor = Math.max(0.05, globalDistance.current / 32);
phaseTimer.current = (0.2 + Math.random() * 1.0) * proximityFactor;
}
}
if (phaseTimer.current <= 0) {
if (movePhase.current === 'frozen') {
movePhase.current = 'lurching';
phaseTimer.current = 0.05 + Math.random() * 0.2;
} else {
movePhase.current = 'frozen';
const proximityFactor = Math.max(0.05, globalDistance.current / 32);
phaseTimer.current = (0.2 + Math.random() * 1.0) * proximityFactor;
}
}
if (movePhase.current === 'lurching') {
globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * 3 * delta;
}
if (movePhase.current === 'lurching') {
globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * 3 * delta;
}
if (audioRef.current && !audioPlaying.current) {
audioPlaying.current = true;
if (audioRef.current.context.state === 'suspended')
audioRef.current.context.resume();
audioRef.current.play();
}
if (audioRef.current && !audioPlaying.current) {
audioPlaying.current = true;
if (audioRef.current.context.state === 'suspended')
audioRef.current.context.resume();
audioRef.current.play();
}
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;
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;
}
}
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 forwardVector = new THREE.Vector3();
camera.getWorldDirection(forwardVector);
const lookDirZ = forwardVector.z < 0 ? -1 : 1;
const calculatedZ = camera.position.z + lookDirZ * globalDistance.current;
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);
creature.position.set(0, 1.6, calculatedZ);
creature.lookAt(camera.position.x, creature.position.y, camera.position.z);
if (!hasTriggered) return;
if (!hasTriggered) return;
const proximity = 1 - Math.max(0, Math.min(1, globalDistance.current / 32));
const jitterX = 1.0 + (Math.random() - 0.5) * 0.04 * proximity;
let jitterY = 1.0 + (Math.random() - 0.5) * 0.06 * proximity;
const proximity = 1 - Math.max(0, Math.min(1, globalDistance.current / 32));
const jitterX = 1.0 + (Math.random() - 0.5) * 0.04 * proximity;
let jitterY = 1.0 + (Math.random() - 0.5) * 0.06 * proximity;
glitchCooldown.current -= delta;
if (glitchCooldown.current <= 0) {
if (Math.random() < 0.25 + proximity * 0.35) {
isGlitchSpiking.current = true;
glitchCooldown.current = 0.03 + Math.random() * 0.08;
} else {
isGlitchSpiking.current = false;
glitchCooldown.current =
0.08 + Math.random() * 0.4 * (1 - proximity * 0.7);
}
}
glitchCooldown.current -= delta;
if (glitchCooldown.current <= 0) {
if (Math.random() < 0.25 + proximity * 0.35) {
isGlitchSpiking.current = true;
glitchCooldown.current = 0.03 + Math.random() * 0.08;
} else {
isGlitchSpiking.current = false;
glitchCooldown.current = 0.08 + Math.random() * 0.4 * (1 - proximity * 0.7);
}
}
if (isGlitchSpiking.current) {
const spike = 0.15 + Math.random() * 0.35;
jitterY += Math.random() > 0.5 ? spike : -spike * 0.6;
}
if (isGlitchSpiking.current) {
const spike = 0.15 + Math.random() * 0.35;
jitterY += Math.random() > 0.5 ? spike : -spike * 0.6;
}
creature.scale.set(jitterX, jitterY, 1.0);
creature.scale.set(jitterX, jitterY, 1.0);
flickerCooldown.current -= delta;
if (flickerCooldown.current <= 0) {
if (creature.visible && Math.random() < 0.12 + proximity * 0.08) {
creature.visible = false;
flickerCooldown.current = 0.02 + Math.random() * 0.05;
} else {
creature.visible = true;
flickerCooldown.current =
0.05 + Math.random() * 0.3 * (1 - proximity * 0.5);
}
}
flickerCooldown.current -= delta;
if (flickerCooldown.current <= 0) {
if (creature.visible && Math.random() < 0.12 + proximity * 0.08) {
creature.visible = false;
flickerCooldown.current = 0.02 + Math.random() * 0.05;
} else {
creature.visible = true;
flickerCooldown.current = 0.05 + Math.random() * 0.3 * (1 - proximity * 0.5);
}
}
texture.offset.set(
(Math.random() - 0.5) * 0.025 * proximity,
(Math.random() - 0.5) * 0.025 * proximity
);
texture.offset.set(
(Math.random() - 0.5) * 0.025 * proximity,
(Math.random() - 0.5) * 0.025 * proximity
);
if (proximity > 0.2) {
creature.position.x += (Math.random() - 0.5) * 0.12 * proximity;
creature.position.y += (Math.random() - 0.5) * 0.06 * proximity;
}
});
if (proximity > 0.2) {
creature.position.x += (Math.random() - 0.5) * 0.12 * proximity;
creature.position.y += (Math.random() - 0.5) * 0.06 * proximity;
}
});
return (
<mesh ref={meshRef} visible={finaleTriggered}>
<planeGeometry args={[3.0, 4.8]} />
<meshStandardMaterial
map={texture}
transparent={true}
depthWrite={false}
side={THREE.DoubleSide}
onBeforeCompile={ShaderPatch}
emissive='#ffffff'
emissiveMap={texture}
emissiveIntensity={0.15}
/>
return (
<mesh
ref={meshRef}
visible={finaleTriggered}
>
<planeGeometry args={[3.0, 4.8]} />
<meshStandardMaterial
map={texture}
transparent={true}
depthWrite={false}
side={THREE.DoubleSide}
onBeforeCompile={ShaderPatch}
emissive="#ffffff"
emissiveMap={texture}
emissiveIntensity={0.15}
/>
{finaleTriggered && (
<PositionalAudio
url="fear/snd/riser.mp3"
ref={audioRef}
distance={25}
loop={false}
autoplay={false}
/>
)}
</mesh>
);
}
{finaleTriggered && (
<PositionalAudio
url='fear/snd/riser.mp3'
ref={audioRef}
distance={25}
loop={false}
autoplay={false}
/>
)}
</mesh>
);
}
+61 -63
View File
@@ -1,84 +1,82 @@
@font-face {
font-family: 'VCR';
src: url('/fear/fonts/vcr.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
font-family: 'VCR';
src: url('/fear/fonts/vcr.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
.finale-container {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0vh;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0vh;
display: grid;
align-items: center;
align-content: center;
justify-content: center;
overflow: hidden;
display: grid;
align-items: center;
align-content: center;
justify-content: center;
overflow: hidden;
/* filter: invert(100%); */
backdrop-filter: brightness(100%);
/* filter: invert(100%); */
backdrop-filter: brightness(100%);
grid-auto-rows: 5vh;
/* grid-template-columns: 0; */
grid-template-rows: repeat(auto-fit, max-content);
grid-auto-rows: 5vh;
/* grid-template-columns: 0; */
grid-template-rows: repeat(auto-fit, max-content);
user-select: none;
will-change: filter;
animation: invertFlicker 0.07s infinite alternate;
user-select: none;
will-change: filter;
animation: invertFlicker 0.07s infinite alternate;
}
@keyframes invertFlicker {
0%,
43%,
45%,
88%,
92% {
filter: invert(0%) contrast(100%) brightness(100%);
backdrop-filter: brightness(100%) hue-rotate(0deg);
}
0%,
43%,
45%,
88%,
92% {
filter: invert(0%) contrast(100%) brightness(100%);
backdrop-filter: brightness(100%) hue-rotate(0deg);
}
44%,
46%,
89%,
93%,
100% {
filter: invert(100%) contrast(300%) brightness(150%);
backdrop-filter: brightness(30%) hue-rotate(180deg) saturate(500%);
}
44%,
46%,
89%,
93%,
100% {
filter: invert(100%) contrast(300%) brightness(150%);
backdrop-filter: brightness(30%) hue-rotate(180deg) saturate(500%);
}
}
.finale-text {
font-family: 'VCR', sans-serif;
font-variant-numeric: tabular-nums;
letter-spacing: 0.1em;
font-family: 'VCR', sans-serif;
font-variant-numeric: tabular-nums;
letter-spacing: 0.1em;
height: 0px;
width: 100%;
color: rgb(255, 255, 255);
font-size: 8vh;
text-align: center;
white-space: nowrap;
height: 0px;
width: 100%;
color: rgb(255, 255, 255);
font-size: 8vh;
text-align: center;
white-space: nowrap;
}
.scanlines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 900;
background: repeating-linear-gradient(rgba(0, 0, 0, 0) 0px,
rgba(0, 0, 0, 0) 2px,
rgba(0, 0, 0, 0.3) 2px,
rgba(0, 0, 0, 0.3) 4px);
}
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 900;
background: repeating-linear-gradient(
rgba(0, 0, 0, 0) 0px,
rgba(0, 0, 0, 0) 2px,
rgba(0, 0, 0, 0.3) 2px,
rgba(0, 0, 0, 0.3) 4px
);
}
+71 -41
View File
@@ -1,56 +1,86 @@
import { JSX, useEffect, useState } from "react"
import { fearState } from "../state"
import { JSX, useEffect, useState } from 'react';
import { fearState } from '../state';
import './finale-text.css';
const BLOCKS = [
"▀", "▂", "▃", "▄", "▅", "▆", "▇",
"█", "▉", "▊", "▋", "▌", "▍", "▎", "▏",
"▐", "░", "▒", "▓", "▔", "▕", "▖", "▗",
"▘", "▙", "▚", "▛", "▜", "▝", "▞", "▟"
'▀',
'▂',
'▃',
'▄',
'▅',
'▆',
'▇',
'█',
'▉',
'▊',
'▋',
'▌',
'▍',
'▎',
'▏',
'▐',
'░',
'▒',
'▓',
'▔',
'▕',
'▖',
'▗',
'▘',
'▙',
'▚',
'▛',
'▜',
'▝',
'▞',
'▟'
];
export default function FinaleText() {
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
const [elements, setElements] = useState<JSX.Element[]>([]);
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
const [elements, setElements] = useState<JSX.Element[]>([]);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWasCaught(fearState.wasCaught)
});
return () => unsubscribe();
}, []);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWasCaught(fearState.wasCaught);
});
return () => unsubscribe();
}, []);
useEffect(() => {
if (!wasCaught) return;
useEffect(() => {
if (!wasCaught)
return;
const interval = setInterval(() => {
if (Math.random() > 0.9) return;
const interval = setInterval(() => {
if (Math.random() > 0.9) return;
const baseText = 'bwaaaaaaaaa';
const corrupted = baseText
.split('')
.map((char) =>
Math.random() > 0.98
? BLOCKS[Math.floor(Math.random() * BLOCKS.length)]
: char
)
.join('');
const baseText = "bwaaaaaaaaa";
const corrupted = baseText
.split("")
.map((char) => (Math.random() > 0.98 ? BLOCKS[Math.floor(Math.random() * BLOCKS.length)] : char))
.join("");
setElements((prev) => [
...prev.slice(-30),
<span className='finale-text' key={crypto.randomUUID()}>
{corrupted}
</span>
]);
}, 10);
setElements((prev) => [...prev.slice(-30),
<span className="finale-text" key={crypto.randomUUID()}>
{corrupted}
</span>
]);
}, 10);
return () => clearInterval(interval);
}, [wasCaught]);
return () => clearInterval(interval);
}, [wasCaught]);
if (!wasCaught) return null;
if (!wasCaught) return null;
return (<>
<div className="finale-container">
{elements}
</div>
<div className="scanlines" />
</>)
}
return (
<>
<div className='finale-container'>{elements}</div>
<div className='scanlines' />
</>
);
}
+472 -341
View File
@@ -1,401 +1,532 @@
import { useEffect, useRef, useState } from "react";
import { FEAR_SETTINGS, fearState } from "../state";
import { useTexture, PositionalAudio } from "@react-three/drei";
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";
import { ShaderPatch } from "../shader-patch";
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
import { ShaderPatch } from '../shader-patch';
interface DoorProps {
position: [number, number, number];
rotation: [number, number, number];
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');
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";
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);
setSoundUrl(chosenSound);
currentSound.current = chosenSound;
}
}, 5000);
return () => clearInterval(interval);
}, []);
return () => clearInterval(interval);
}, []);
const handleAudioEnded = () => {
setSoundUrl(null);
currentSound.current = null;
};
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>
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>
{/* 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>
{/* handle */}
<mesh position={[0.75, 1.8, 0.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>
);
{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'
]);
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]);
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 segmentPool = [0, 1, 2, 3, 4];
const segmentCount = segmentPool.length;
const lightRefs = useRef<(THREE.PointLight | null)[]>([]);
const matRefs = useRef<(THREE.MeshStandardMaterial | null)[]>([]);
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 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[]>([]);
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 = [];
wallMaterialsRef.current = [];
floorMaterialsRef.current = [];
pipeMaterialsRef.current = [];
bracketMaterialsRef.current = [];
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWidth(fearState.currentWidth);
setIsRustActive(fearState.isRustActive);
});
return () => unsubscribe();
}, []);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWidth(fearState.currentWidth);
setIsRustActive(fearState.isRustActive);
});
return () => unsubscribe();
}, []);
useFrame((state, delta) => {
const time = state.clock.elapsedTime;
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;
}
}
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 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);
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;
floorTex.needsUpdate = true;
wallTex.needsUpdate = true;
rustWallTex.needsUpdate = true;
rustFloorTex.needsUpdate = true;
let closestPoolIndex = 0;
let minDistance = Infinity;
let closestPoolIndex = 0;
let minDistance = Infinity;
segmentsRef.current.forEach((segGroup, poolIndex) => {
if (!segGroup) return;
segmentsRef.current.forEach((segGroup, poolIndex) => {
if (!segGroup) return;
let segmentZIndex = poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ;
segGroup.position.z = segmentZIndex * length;
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 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 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 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 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;
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;
});
}
});
for (let i = 0; i < 3; i++) {
const pipe = segGroup.getObjectByName(`pipe-${i}`);
if (pipe) pipe.position.x = -width / 2 + 0.4 + i * 0.2;
}
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];
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);
}
}
});
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;
});
};
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);
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;
});
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;
});
});
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>
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>
{/* 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>
{/* 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>
{/* 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>
{/* 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>
))}
{/* 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.2,
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>
))}
</>
);
{/* 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>
))}
</>
);
}
+173 -145
View File
@@ -1,8 +1,8 @@
import { useFrame, useThree } from "@react-three/fiber";
import { useEffect, useRef } from "react";
import { FEAR_SETTINGS, fearState } from "../state";
import { PointerLockControls } from "@react-three/drei";
import * as THREE from "three";
import { useFrame, useThree } from '@react-three/fiber';
import { useEffect, useRef } from 'react';
import { FEAR_SETTINGS, fearState } from '../state';
import { PointerLockControls } from '@react-three/drei';
import * as THREE from 'three';
const forward = new THREE.Vector3();
const side = new THREE.Vector3();
@@ -14,181 +14,209 @@ const targetVelocity = new THREE.Vector3();
const currentVelocity = new THREE.Vector3();
function usePlayerControls() {
const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false });
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;
};
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;
};
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);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, []);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, []);
return keys.current;
return keys.current;
}
export default function Player() {
const { camera } = useThree();
const controls = usePlayerControls();
const { camera } = useThree();
const controls = usePlayerControls();
const flashlightRef = useRef<THREE.SpotLight>(null);
const movementCounter = useRef<number>(0);
const bobIntensity = useRef<number>(0);
const flashlightRef = useRef<THREE.SpotLight>(null);
const movementCounter = useRef<number>(0);
const bobIntensity = useRef<number>(0);
const confirmedSegment = useRef<number>(0);
const hasTriggeredThisSegment = useRef<boolean>(false);
const confirmedSegment = useRef<number>(0);
const hasTriggeredThisSegment = useRef<boolean>(false);
const footstepAudio = useRef<HTMLAudioElement[]>([]);
const hasStepped = useRef<boolean>(false);
const footstepAudio = useRef<HTMLAudioElement[]>([]);
const hasStepped = useRef<boolean>(false);
useEffect(() => {
playerRoot.set(
camera.position.x,
FEAR_SETTINGS.PLAYER_HEIGHT,
camera.position.z
);
footstepAudio.current = Array.from({ length: 6 }, (_, i) => {
const audio = new Audio(`fear/snd/footstep${i + 1}.mp3`);
audio.volume = 0.4;
return audio;
});
}, []);
const playRandomFootstep = () => {
if (footstepAudio.current.length === 0) return;
useEffect(() => {
playerRoot.set(camera.position.x, FEAR_SETTINGS.PLAYER_HEIGHT, camera.position.z);
footstepAudio.current = Array.from({ length: 6 }, (_, i) => {
const audio = new Audio(`fear/snd/footstep${i + 1}.mp3`);
audio.volume = 0.4;
return audio;
});
}, []);
const randomIndex = Math.floor(
Math.random() * footstepAudio.current.length
);
const audio = footstepAudio.current[randomIndex];
const playRandomFootstep = () => {
if (footstepAudio.current.length === 0) return;
audio.currentTime = 0;
audio.play().catch((err) => {
console.warn(
'Footstep playback blocked by browser autocomplete/interaction rules.',
err
);
});
};
const randomIndex = Math.floor(Math.random() * footstepAudio.current.length);
const audio = footstepAudio.current[randomIndex];
useFrame((state, delta) => {
const dt = Math.min(delta, 0.1);
audio.currentTime = 0;
audio.play().catch((err) => {
console.warn("Footstep playback blocked by browser autocomplete/interaction rules.", err);
});
};
camera.getWorldDirection(forward);
forward.y = 0;
forward.normalize();
side.crossVectors(forward, THREE.Object3D.DEFAULT_UP).normalize();
useFrame((state, delta) => {
const dt = Math.min(delta, 0.1);
const moveForward = Number(controls.Forward) - Number(controls.Backward);
const moveSide = Number(controls.Right) - Number(controls.Left);
camera.getWorldDirection(forward);
forward.y = 0;
forward.normalize();
side.crossVectors(forward, THREE.Object3D.DEFAULT_UP).normalize();
targetVelocity.set(0, 0, 0);
if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward);
if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide);
const moveForward = Number(controls.Forward) - Number(controls.Backward);
const moveSide = Number(controls.Right) - Number(controls.Left);
if (targetVelocity.lengthSq() > 0)
targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED);
targetVelocity.set(0, 0, 0);
if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward);
if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide);
currentVelocity.lerp(targetVelocity, 10 * dt);
if (targetVelocity.lengthSq() > 0)
targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED);
playerRoot.x += currentVelocity.x * dt;
playerRoot.z += currentVelocity.z * dt;
currentVelocity.lerp(targetVelocity, 10 * dt);
const minX = -fearState.currentWidth / 2 + FEAR_SETTINGS.WALL_BUFFER;
const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER;
playerRoot.x = THREE.MathUtils.clamp(playerRoot.x, minX, maxX);
playerRoot.x += currentVelocity.x * dt;
playerRoot.z += currentVelocity.z * dt;
const isMoving =
controls.Forward || controls.Backward || controls.Left || controls.Right;
const minX = -fearState.currentWidth / 2 + FEAR_SETTINGS.WALL_BUFFER;
const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER;
playerRoot.x = THREE.MathUtils.clamp(playerRoot.x, minX, maxX);
bobIntensity.current = THREE.MathUtils.lerp(
bobIntensity.current,
isMoving ? 1 : 0,
8 * dt
);
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right;
if (isMoving) movementCounter.current += dt * 12;
bobIntensity.current = THREE.MathUtils.lerp(bobIntensity.current, isMoving ? 1 : 0, 8 * dt);
const sinWave = Math.sin(movementCounter.current);
const moveBobY = sinWave * 0.06 * bobIntensity.current;
const moveBobX =
Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current;
if (isMoving)
movementCounter.current += dt * 12;
if (isMoving && sinWave < -0.9) {
if (!hasStepped.current) {
playRandomFootstep();
hasStepped.current = true;
}
} else if (sinWave > 0) {
hasStepped.current = false;
}
const sinWave = Math.sin(movementCounter.current);
const moveBobY = sinWave * 0.06 * bobIntensity.current;
const moveBobX = Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current;
const breatheTime = state.clock.elapsedTime * 1.8;
const breatheBobY =
Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5);
if (isMoving && sinWave < -0.9) {
if (!hasStepped.current) {
playRandomFootstep();
hasStepped.current = true;
}
} else if (sinWave > 0) {
hasStepped.current = false;
}
camera.position.copy(playerRoot);
camera.position.y += moveBobY + breatheBobY;
camera.position.addScaledVector(side, moveBobX);
const breatheTime = state.clock.elapsedTime * 1.8;
const breatheBobY = Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5);
if (flashlightRef.current) {
flashlightRef.current.position.lerp(camera.position, 7 * dt);
camera.getWorldDirection(viewDirection);
camera.position.copy(playerRoot);
camera.position.y += moveBobY + breatheBobY;
camera.position.addScaledVector(side, moveBobX);
targetDest.copy(camera.position).addScaledVector(viewDirection, 10);
if (flashlightRef.current) {
flashlightRef.current.position.lerp(camera.position, 7 * dt);
camera.getWorldDirection(viewDirection);
flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
flashlightRef.current.target.updateMatrixWorld();
targetDest
.copy(camera.position)
.addScaledVector(viewDirection, 10);
flashlightRef.current.intensity =
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
Math.sin(state.clock.elapsedTime * 30) *
0.15 *
Math.cos(state.clock.elapsedTime * 3);
}
flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
flashlightRef.current.target.updateMatrixWorld();
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
const absoluteZ = -playerRoot.z;
const rawSegmentIndex = Math.floor(absoluteZ / length);
const progressZ = (((absoluteZ % length) + length) % length) / length;
flashlightRef.current.intensity =
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
Math.sin(state.clock.elapsedTime * 30) * 0.15 * Math.cos(state.clock.elapsedTime * 3);
}
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;
}
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
const absoluteZ = -playerRoot.z;
const rawSegmentIndex = Math.floor(absoluteZ / length);
const progressZ = ((absoluteZ % length) + length) % length / length;
if (
rawSegmentIndex === confirmedSegment.current &&
progressZ > 0.35 &&
progressZ < 0.65
) {
hasTriggeredThisSegment.current = false;
}
});
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={25}
angle={0.35}
penumbra={0.8}
intensity={0}
color="#fffaed"
decay={2}
castShadow
shadow-bias={-0.001}
/>
</>
);
}
return (
<>
<PointerLockControls />
<spotLight
ref={flashlightRef}
distance={25}
angle={0.35}
penumbra={0.8}
intensity={0}
color='#fffaed'
decay={2}
castShadow
shadow-bias={-0.001}
/>
</>
);
}
+15 -11
View File
@@ -1,5 +1,9 @@
export function ShaderPatch(shader: { vertexShader: string, fragmentShader: string, uniforms: Object }) {
shader.vertexShader = `
export function ShaderPatch(shader: {
vertexShader: string;
fragmentShader: string;
uniforms: Object;
}) {
shader.vertexShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
@@ -7,9 +11,9 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri
${shader.vertexShader}
`;
shader.vertexShader = shader.vertexShader.replace(
`#include <project_vertex>`,
`
shader.vertexShader = shader.vertexShader.replace(
`#include <project_vertex>`,
`
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
@@ -24,9 +28,9 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri
vAffineUv = vMapUv * gl_Position.w;
#endif
`
);
);
shader.fragmentShader = `
shader.fragmentShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
@@ -34,9 +38,9 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri
${shader.fragmentShader}
`;
shader.fragmentShader = shader.fragmentShader.replace(
`#include <map_fragment>`,
`
shader.fragmentShader = shader.fragmentShader.replace(
`#include <map_fragment>`,
`
#ifdef USE_MAP
vec2 flatAffineUV = vAffineUv / max(vDepth, 0.001);
@@ -53,5 +57,5 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri
diffuseColor *= texelColor;
#endif
`
);
);
}
+65 -53
View File
@@ -1,74 +1,86 @@
import * as THREE from 'three';
export const FEAR_SETTINGS = {
HALLWAY_LENGTH: 40,
HALLWAY_WIDTH: 6,
HALLWAY_HEIGHT: 5,
PLAYER_HEIGHT: 3,
PLAYER_SPEED: 4,
FLASHLIGHT_INTENSITY_BASE: 8,
WALL_BUFFER: 0.6,
CREATURE_SPEED: 8,
HALLWAY_LENGTH: 40,
HALLWAY_WIDTH: 6,
HALLWAY_HEIGHT: 5,
PLAYER_HEIGHT: 3,
PLAYER_SPEED: 4,
FLASHLIGHT_INTENSITY_BASE: 8,
WALL_BUFFER: 0.6,
CREATURE_SPEED: 8,
EVENT_NARROW_LOOP_COUNT: 2,
EVENT_RUST_LOOP_COUNT: 4,
EVENT_FINALE_LOOP_COUNT: 5,
EVENT_NARROW_LOOP_COUNT: 2,
EVENT_RUST_LOOP_COUNT: 4,
EVENT_FINALE_LOOP_COUNT: 5,
EVENT_FINALE_DURATION: 1,
EVENT_FINALE_DURATION: 1,
TEST_MODE: false
TEST_MODE: false
};
const listeners = new Set<() => void>();
export const fearState = {
loopCount: 0,
currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH,
isRustActive: false,
finaleTriggered: false,
wasCaught: false,
finaleProgression: 0,
loopCount: 0,
currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH,
isRustActive: false,
finaleTriggered: false,
wasCaught: false,
finaleProgression: 0,
subscribe(listener: () => void) {
listeners.add(listener);
return () => { listeners.delete(listener); };
},
subscribe(listener: () => void) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
},
emit() {
listeners.forEach((listener) => listener());
},
emit() {
listeners.forEach((listener) => listener());
},
update(delta: number) {
const targetWidth = this.loopCount >= FEAR_SETTINGS.EVENT_NARROW_LOOP_COUNT ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH;
const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta);
update(delta: number) {
const targetWidth =
this.loopCount >= FEAR_SETTINGS.EVENT_NARROW_LOOP_COUNT
? 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;
}
if (Math.abs(this.currentWidth - newWidth) > 0.001) {
this.currentWidth = newWidth;
}
if (this.wasCaught) {
if (this.finaleProgression < FEAR_SETTINGS.EVENT_FINALE_DURATION) {
if (this.wasCaught) {
if (this.finaleProgression < FEAR_SETTINGS.EVENT_FINALE_DURATION) {
this.finaleProgression = Math.min(
this.finaleProgression + delta,
FEAR_SETTINGS.EVENT_FINALE_DURATION
);
} else {
window.location.href = '/';
}
}
this.finaleProgression = Math.min(this.finaleProgression + delta, FEAR_SETTINGS.EVENT_FINALE_DURATION);
} else {
window.location.href = '/';
}
}
this.emit();
},
this.emit();
},
registerLoop(direction: 'forward' | 'backward') {
this.loopCount += 1;
registerLoop(direction: 'forward' | 'backward') {
this.loopCount += 1;
this.isRustActive = this.loopCount >= FEAR_SETTINGS.EVENT_RUST_LOOP_COUNT;
this.finaleTriggered =
this.loopCount >= FEAR_SETTINGS.EVENT_FINALE_LOOP_COUNT;
this.isRustActive = this.loopCount >= FEAR_SETTINGS.EVENT_RUST_LOOP_COUNT;
this.finaleTriggered = this.loopCount >= FEAR_SETTINGS.EVENT_FINALE_LOOP_COUNT;
this.emit();
},
this.emit();
},
registerCaught() {
this.wasCaught = true;
this.emit();
}
};
registerCaught() {
this.wasCaught = true;
this.emit();
}
};