Compare commits

...

18 Commits

Author SHA1 Message Date
neru ab3bf047d4 style: reduce noise opacity 2026-06-02 04:43:52 -03:00
neru f1ab2b692d style: change text 2026-06-02 04:43:09 -03:00
neru 673aabce50 style: move patch to its own file 2026-06-02 04:42:16 -03:00
neru b3a5712c85 feat: make movement jitter, add phase effects 2026-06-02 04:42:07 -03:00
neru 4691a9fbf4 feat: add flicker anim 2026-06-02 04:30:03 -03:00
neru 0fca4db440 style: change size and alignment 2026-06-02 04:13:54 -03:00
neru 930139d1df feat: add unicode glitches 2026-06-02 04:13:29 -03:00
neru 4120e5ec72 feat: move href logic to state mgr 2026-06-02 03:57:42 -03:00
neru fd314cf2ec feat: make finale text infinite 2026-06-02 03:57:31 -03:00
neru e3ab974988 style: reduce fov 2026-06-02 03:45:04 -03:00
neru e4a0c57e79 feat: add TEST_MODE 2026-06-02 03:44:58 -03:00
neru ebda4b281e feat: add steel texture for doors 2026-06-02 03:29:17 -03:00
neru 8dcc888d5c feat: add psx style vertex snapping and affine distortion 2026-06-02 03:29:07 -03:00
neru b7e61b4240 feat: lerp flashlight pos 2026-06-02 02:50:11 -03:00
neru 23c39a71a6 feat: add footsteps 2026-06-02 02:50:04 -03:00
neru a0ee50703c feat: add better ambience sound 2026-06-02 02:44:44 -03:00
neru 8c4080f10c feat: add finale text 2026-06-01 22:02:22 -03:00
neru b9eeed848b feat: add door knocks 2026-06-01 22:02:12 -03:00
19 changed files with 412 additions and 47 deletions
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11 -8
View File
@@ -8,13 +8,14 @@ import { Suspense, useEffect, useState } from "react";
import { AmbientSound } from './scene-components/ambient-sound'; import { AmbientSound } from './scene-components/ambient-sound';
import { fearState } from './state'; import { FEAR_SETTINGS, fearState } from './state';
import TheCreature from './scene-components/creature'; import TheCreature from './scene-components/creature';
import Player from './scene-components/player'; import Player from './scene-components/player';
import Hallway from './scene-components/hallway'; import Hallway from './scene-components/hallway';
import { AudioListener } from 'three'; import { AudioListener } from 'three';
import FinaleText from './scene-components/finale-text';
function PostProcessing() { function PostProcessing() {
const [wasCaught, setWasCaught] = useState(fearState.wasCaught); const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
@@ -27,9 +28,9 @@ function PostProcessing() {
}, []); }, []);
return (<EffectComposer> return (<EffectComposer>
<Pixelation granularity={wasCaught ? 18 : 12} /> <Pixelation granularity={wasCaught ? 18 : 10} />
<Vignette /> <Vignette />
<Noise opacity={wasCaught ? 0.01 : 0.005} /> <Noise opacity={wasCaught ? 0.01 : 0.003} />
<BrightnessContrast <BrightnessContrast
brightness={-0.01} brightness={-0.01}
contrast={0.05} contrast={0.05}
@@ -77,7 +78,7 @@ export default function Fear() {
shadows shadows
gl={{ antialias: true }} gl={{ antialias: true }}
className='canvas' className='canvas'
camera={{ position: [0, 3, -5], fov: 65, far: 100 }} camera={{ position: [0, 3, -5], fov: 55, far: 100 }}
> >
<FearStateUpdater /> <FearStateUpdater />
@@ -85,9 +86,9 @@ export default function Fear() {
<color attach="background" args={['#050505']} /> <color attach="background" args={['#050505']} />
<ambientLight intensity={0.0225} /> {FEAR_SETTINGS.TEST_MODE ? <ambientLight intensity={2} /> : <ambientLight intensity={0.0225} />}
<fogExp2 attach='fog' args={[0x050505, 0.035]} /> {FEAR_SETTINGS.TEST_MODE ? null : <fogExp2 attach='fog' args={[0x050505, 0.035]} />}
<PostProcessing /> {FEAR_SETTINGS.TEST_MODE ? null : < PostProcessing />}
<Suspense fallback={null}> <Suspense fallback={null}>
<Hallway /> <Hallway />
@@ -98,7 +99,7 @@ export default function Fear() {
<AmbientSound <AmbientSound
key="ambient-1" key="ambient-1"
url='fear/snd/ambience.mp3' url='fear/snd/ambience.mp3'
volume={isRustActive ? 0 : 1} volume={isRustActive ? 0 : 0.5}
/> />
<AmbientSound <AmbientSound
@@ -113,5 +114,7 @@ export default function Fear() {
volume={1} volume={1}
/> : null} /> : null}
</Canvas> </Canvas>
<FinaleText />
</>) </>)
} }
+89 -6
View File
@@ -1,14 +1,22 @@
import { useTexture, PositionalAudio } from "@react-three/drei"; import { useTexture, PositionalAudio } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber"; import { useFrame, useThree } from "@react-three/fiber";
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import * as THREE from "three"; import * as THREE from "three";
import { FEAR_SETTINGS, fearState } from "../state"; import { FEAR_SETTINGS, fearState } from "../state";
import { ShaderPatch } from "../shader-patch";
useTexture.preload('fear/img/creature.png'); useTexture.preload('fear/img/creature.png');
export default function TheCreature() { export default function TheCreature() {
const texture = 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 meshRef = useRef<THREE.Mesh>(null); const meshRef = useRef<THREE.Mesh>(null);
const audioRef = useRef<THREE.PositionalAudio>(null); const audioRef = useRef<THREE.PositionalAudio>(null);
const { camera } = useThree(); const { camera } = useThree();
@@ -21,6 +29,13 @@ export default function TheCreature() {
const audioPlaying = useRef<boolean>(false); const audioPlaying = useRef<boolean>(false);
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);
useEffect(() => { useEffect(() => {
const unsubscribe = fearState.subscribe(() => { const unsubscribe = fearState.subscribe(() => {
setFinaleTriggered(fearState.finaleTriggered); setFinaleTriggered(fearState.finaleTriggered);
@@ -30,6 +45,8 @@ export default function TheCreature() {
setHasTriggered(false); setHasTriggered(false);
globalDistance.current = 32; globalDistance.current = 32;
audioPlaying.current = false; audioPlaying.current = false;
movePhase.current = 'frozen';
phaseTimer.current = 1.5;
if (audioRef.current && audioRef.current.isPlaying) if (audioRef.current && audioRef.current.isPlaying)
audioRef.current.stop(); audioRef.current.stop();
@@ -47,16 +64,33 @@ export default function TheCreature() {
if (!isSpawned) { if (!isSpawned) {
setIsSpawned(true); setIsSpawned(true);
globalDistance.current = 32; globalDistance.current = 32;
movePhase.current = 'frozen';
phaseTimer.current = 1.0 + Math.random() * 1.5;
} }
if (!hasTriggered) { if (!hasTriggered) {
if (globalDistance.current < 40) { if (globalDistance.current < 40)
setHasTriggered(true); setHasTriggered(true);
}
} }
if (hasTriggered) { if (hasTriggered) {
globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * delta; 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 (movePhase.current === 'lurching') {
globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * 3 * delta;
}
if (audioRef.current && !audioPlaying.current) { if (audioRef.current && !audioPlaying.current) {
audioPlaying.current = true; audioPlaying.current = true;
@@ -84,6 +118,51 @@ export default function TheCreature() {
creature.position.set(0, 1.6, calculatedZ); creature.position.set(0, 1.6, calculatedZ);
creature.lookAt(camera.position.x, creature.position.y, camera.position.z); creature.lookAt(camera.position.x, creature.position.y, camera.position.z);
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;
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;
}
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);
}
}
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;
}
}); });
return ( return (
@@ -92,11 +171,15 @@ export default function TheCreature() {
visible={finaleTriggered} visible={finaleTriggered}
> >
<planeGeometry args={[3.0, 4.8]} /> <planeGeometry args={[3.0, 4.8]} />
<meshBasicMaterial <meshStandardMaterial
map={texture} map={texture}
transparent={true} transparent={true}
depthWrite={false} depthWrite={false}
side={THREE.DoubleSide} side={THREE.DoubleSide}
onBeforeCompile={ShaderPatch}
emissive="#ffffff"
emissiveMap={texture}
emissiveIntensity={0.15}
/> />
{finaleTriggered && ( {finaleTriggered && (
@@ -0,0 +1,84 @@
@font-face {
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;
display: grid;
align-items: center;
align-content: center;
justify-content: center;
overflow: hidden;
/* filter: invert(100%); */
backdrop-filter: brightness(100%);
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;
}
@keyframes invertFlicker {
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%);
}
}
.finale-text {
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;
}
.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);
}
@@ -0,0 +1,56 @@
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[]>([]);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWasCaught(fearState.wasCaught)
});
return () => unsubscribe();
}, []);
useEffect(() => {
if (!wasCaught)
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("");
setElements((prev) => [...prev.slice(-30),
<span className="finale-text" key={crypto.randomUUID()}>
{corrupted}
</span>
]);
}, 10);
return () => clearInterval(interval);
}, [wasCaught]);
if (!wasCaught) return null;
return (<>
<div className="finale-container">
{elements}
</div>
<div className="scanlines" />
</>)
}
+57 -22
View File
@@ -1,31 +1,67 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { FEAR_SETTINGS, fearState } from "../state"; import { FEAR_SETTINGS, fearState } from "../state";
import { useTexture } from "@react-three/drei"; import { useTexture, PositionalAudio } from "@react-three/drei";
import * as THREE from "three"; import * as THREE from "three";
import { useFrame } from "@react-three/fiber"; import { useFrame } from "@react-three/fiber";
import { ShaderPatch } from "../shader-patch";
interface DoorProps { interface DoorProps {
position: [number, number, number]; position: [number, number, number];
rotation: [number, number, number]; rotation: [number, number, number];
} }
function Door({ position, rotation }: DoorProps) { function Door({ position, rotation }: DoorProps) {
const [soundUrl, setSoundUrl] = useState<string | null>(null);
const currentSound = useRef<string | null>(null);
const steelTex = useTexture('fear/img/steel.png');
useEffect(() => {
const interval = setInterval(() => {
if (Math.random() < 0.02) {
const chosenSound = Math.random() < 0.5 ? "fear/snd/knock1.mp3" : "fear/snd/knock2.mp3";
setSoundUrl(chosenSound);
currentSound.current = chosenSound;
}
}, 5000);
return () => clearInterval(interval);
}, []);
const handleAudioEnded = () => {
setSoundUrl(null);
currentSound.current = null;
};
return ( return (
<group position={position} rotation={rotation}> <group position={position} rotation={rotation}>
<mesh position={[0, 2, -0.14]}> {/* frame */}
<boxGeometry args={[2.4, 4.0, 0.2]} /> <mesh position={[0, 2, -0.1]}>
<meshStandardMaterial color="#8a8585" roughness={0.8} metalness={0.2} /> <boxGeometry args={[2.4, 4.0, 0.2, 4, 4, 1]} />
<meshStandardMaterial map={steelTex} color="#8d8d8d" onBeforeCompile={ShaderPatch} />
</mesh> </mesh>
<mesh position={[0, 1.95, -0.08]}> {/* panel */}
<boxGeometry args={[2.1, 3.8, 0.1]} /> <mesh position={[0, 1.95, -0.0]}>
<meshStandardMaterial color="#4e4b4b" roughness={0.7} metalness={0.2} /> <boxGeometry args={[2.1, 3.8, 0.1, 4, 4, 1]} />
<meshStandardMaterial map={steelTex} color="#4e4a4a" onBeforeCompile={ShaderPatch} />
</mesh> </mesh>
<mesh position={[0.9, 1.8, 0.08]}> {/* handle */}
<boxGeometry args={[0.08, 0.08, 0.15]} /> <mesh position={[0.75, 1.8, .085]}>
<meshStandardMaterial color="#4e4b4b" roughness={0.4} metalness={0.2} /> <boxGeometry args={[0.3, 0.08, 0.1]} />
<meshStandardMaterial map={steelTex} color="#ffffff" onBeforeCompile={ShaderPatch} />
</mesh> </mesh>
{soundUrl && (
<PositionalAudio
url={soundUrl}
distance={25}
loop={false}
autoplay={true}
onEnded={handleAudioEnded}
/>
)}
</group> </group>
); );
} }
@@ -252,6 +288,7 @@ export default function Hallway() {
emissive="#a8a1a1" emissive="#a8a1a1"
emissiveIntensity={0.8} emissiveIntensity={0.8}
roughness={0.9} roughness={0.9}
onBeforeCompile={ShaderPatch}
/> />
</mesh> </mesh>
</group> </group>
@@ -262,12 +299,11 @@ export default function Hallway() {
rotation={[-Math.PI / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}
position={[0, 0, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]} position={[0, 0, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
> >
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH]} /> <planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH, 4, 10]} />
<meshStandardMaterial <meshStandardMaterial
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }} ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
map={floorTex} map={floorTex}
roughness={0.8} onBeforeCompile={ShaderPatch}
metalness={0.2}
/> />
</mesh> </mesh>
@@ -277,24 +313,22 @@ export default function Hallway() {
rotation={[Math.PI / 2, 0, 0]} rotation={[Math.PI / 2, 0, 0]}
position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
> >
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH]} /> <planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH, 4, 10]} />
<meshStandardMaterial <meshStandardMaterial
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }} ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
map={floorTex} map={floorTex}
roughness={0.8} onBeforeCompile={ShaderPatch}
metalness={0.2}
/> />
</mesh> </mesh>
{/* left wall */} {/* left wall */}
<group name="left-wall-group"> <group name="left-wall-group">
<mesh rotation={[0, Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}> <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]} /> <planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT, 10, 4]} />
<meshStandardMaterial <meshStandardMaterial
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }} ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
map={wallTex} map={wallTex}
roughness={0.7} onBeforeCompile={ShaderPatch}
metalness={0.1}
/> />
</mesh> </mesh>
{!isRustActive && ( {!isRustActive && (
@@ -308,12 +342,11 @@ export default function Hallway() {
{/* right wall */} {/* right wall */}
<group name="right-wall-group"> <group name="right-wall-group">
<mesh rotation={[0, -Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}> <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]} /> <planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT, 10, 4]} />
<meshStandardMaterial <meshStandardMaterial
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }} ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
map={wallTex} map={wallTex}
roughness={0.7} onBeforeCompile={ShaderPatch}
metalness={0.1}
/> />
</mesh> </mesh>
{!isRustActive && ( {!isRustActive && (
@@ -335,6 +368,7 @@ export default function Hallway() {
color="#a5aca8" color="#a5aca8"
roughness={0.0} roughness={0.0}
metalness={0.4} metalness={0.4}
onBeforeCompile={ShaderPatch}
/> />
</mesh> </mesh>
))} ))}
@@ -354,6 +388,7 @@ export default function Hallway() {
color="#a5aca8" color="#a5aca8"
roughness={0.0} roughness={0.0}
metalness={0.4} metalness={0.4}
onBeforeCompile={ShaderPatch}
/> />
</mesh> </mesh>
); );
+34 -2
View File
@@ -54,10 +54,32 @@ export default function Player() {
const confirmedSegment = useRef<number>(0); const confirmedSegment = useRef<number>(0);
const hasTriggeredThisSegment = useRef<boolean>(false); const hasTriggeredThisSegment = useRef<boolean>(false);
const footstepAudio = useRef<HTMLAudioElement[]>([]);
const hasStepped = useRef<boolean>(false);
useEffect(() => { useEffect(() => {
playerRoot.set(camera.position.x, FEAR_SETTINGS.PLAYER_HEIGHT, camera.position.z); 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;
const randomIndex = Math.floor(Math.random() * footstepAudio.current.length);
const audio = footstepAudio.current[randomIndex];
audio.currentTime = 0;
audio.play().catch((err) => {
console.warn("Footstep playback blocked by browser autocomplete/interaction rules.", err);
});
};
useFrame((state, delta) => { useFrame((state, delta) => {
const dt = Math.min(delta, 0.1); const dt = Math.min(delta, 0.1);
@@ -92,9 +114,19 @@ export default function Player() {
if (isMoving) if (isMoving)
movementCounter.current += dt * 12; movementCounter.current += dt * 12;
const moveBobY = Math.sin(movementCounter.current) * 0.06 * bobIntensity.current; 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 moveBobX = Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current;
if (isMoving && sinWave < -0.9) {
if (!hasStepped.current) {
playRandomFootstep();
hasStepped.current = true;
}
} else if (sinWave > 0) {
hasStepped.current = false;
}
const breatheTime = state.clock.elapsedTime * 1.8; const breatheTime = state.clock.elapsedTime * 1.8;
const breatheBobY = Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5); const breatheBobY = Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5);
@@ -103,7 +135,7 @@ export default function Player() {
camera.position.addScaledVector(side, moveBobX); camera.position.addScaledVector(side, moveBobX);
if (flashlightRef.current) { if (flashlightRef.current) {
flashlightRef.current.position.copy(camera.position); flashlightRef.current.position.lerp(camera.position, 7 * dt);
camera.getWorldDirection(viewDirection); camera.getWorldDirection(viewDirection);
targetDest targetDest
+57
View File
@@ -0,0 +1,57 @@
export function ShaderPatch(shader: { vertexShader: string, fragmentShader: string, uniforms: Object }) {
shader.vertexShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
#endif
${shader.vertexShader}
`;
shader.vertexShader = shader.vertexShader.replace(
`#include <project_vertex>`,
`
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
float precisionModifier = 200.0;
gl_Position.xy /= gl_Position.w;
gl_Position.xy = floor(gl_Position.xy * precisionModifier) / precisionModifier;
gl_Position.xy *= gl_Position.w;
vDepth = gl_Position.w;
#ifdef USE_MAP
vAffineUv = vMapUv * gl_Position.w;
#endif
`
);
shader.fragmentShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
#endif
${shader.fragmentShader}
`;
shader.fragmentShader = shader.fragmentShader.replace(
`#include <map_fragment>`,
`
#ifdef USE_MAP
vec2 flatAffineUV = vAffineUv / max(vDepth, 0.001);
vec2 warpDiff = flatAffineUV - vMapUv;
float warpDist = length(warpDiff);
float maxDistortion = 0.25;
float falloff = maxDistortion / (maxDistortion + warpDist);
vec2 finalUV = vMapUv + (warpDiff * falloff);
vec4 texelColor = texture2D( map, finalUV );
diffuseColor *= texelColor;
#endif
`
);
}
+17 -2
View File
@@ -12,7 +12,11 @@ export const FEAR_SETTINGS = {
EVENT_NARROW_LOOP_COUNT: 2, EVENT_NARROW_LOOP_COUNT: 2,
EVENT_RUST_LOOP_COUNT: 4, EVENT_RUST_LOOP_COUNT: 4,
EVENT_FINALE_LOOP_COUNT: 5 EVENT_FINALE_LOOP_COUNT: 5,
EVENT_FINALE_DURATION: 1,
TEST_MODE: false
}; };
const listeners = new Set<() => void>(); const listeners = new Set<() => void>();
@@ -23,6 +27,7 @@ export const fearState = {
isRustActive: false, isRustActive: false,
finaleTriggered: false, finaleTriggered: false,
wasCaught: false, wasCaught: false,
finaleProgression: 0,
subscribe(listener: () => void) { subscribe(listener: () => void) {
listeners.add(listener); listeners.add(listener);
@@ -39,8 +44,18 @@ export const fearState = {
if (Math.abs(this.currentWidth - newWidth) > 0.001) { if (Math.abs(this.currentWidth - newWidth) > 0.001) {
this.currentWidth = newWidth; this.currentWidth = newWidth;
this.emit();
} }
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.emit();
}, },
registerLoop(direction: 'forward' | 'backward') { registerLoop(direction: 'forward' | 'backward') {