Compare commits

...

12 Commits

Author SHA1 Message Date
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
15 changed files with 213 additions and 62 deletions
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.
+7 -7
View File
@@ -8,7 +8,7 @@ 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';
@@ -28,7 +28,7 @@ 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.005} />
<BrightnessContrast <BrightnessContrast
@@ -78,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 />
@@ -86,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 />
@@ -99,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
@@ -70,7 +70,6 @@ export default function TheCreature() {
camera.position.y += (Math.random() - 0.5) * shakeIntensity; camera.position.y += (Math.random() - 0.5) * shakeIntensity;
if (globalDistance.current <= 0.1) { if (globalDistance.current <= 0.1) {
window.location.href = '/';
fearState.registerCaught(); fearState.registerCaught();
return; return;
} }
+34 -2
View File
@@ -19,22 +19,54 @@
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
grid-auto-rows: 2.5vh; /* filter: invert(100%); */
backdrop-filter: brightness(100%);
grid-auto-rows: 5vh;
/* grid-template-columns: 0; */ /* grid-template-columns: 0; */
grid-template-rows: repeat(auto-fit, max-content); grid-template-rows: repeat(auto-fit, max-content);
user-select: none; 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 { .finale-text {
font-family: 'VCR', sans-serif; font-family: 'VCR', sans-serif;
font-variant-numeric: tabular-nums;
letter-spacing: 0.1em;
height: 0px; height: 0px;
width: 100%; width: 100%;
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
font-size: 5vh; font-size: 8vh;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
} }
.scanlines { .scanlines {
+33 -13
View File
@@ -1,36 +1,56 @@
import { JSX, useEffect, useState } from "react" import { JSX, useEffect, useState } from "react"
import { FEAR_SETTINGS, fearState } from "../state" import { fearState } from "../state"
import './finale-text.css'; import './finale-text.css';
const BLOCKS = [
"▀", "▂", "▃", "▄", "▅", "▆", "▇",
"█", "▉", "▊", "▋", "▌", "▍", "▎", "▏",
"▐", "░", "▒", "▓", "▔", "▕", "▖", "▗",
"▘", "▙", "▚", "▛", "▜", "▝", "▞", "▟"
];
export default function FinaleText() { export default function FinaleText() {
const [progression, setProgression] = useState(fearState.finaleProgression); const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
const [wasCaught, setWasCaught] = useState(fearState.isRustActive); const [elements, setElements] = useState<JSX.Element[]>([]);
useEffect(() => { useEffect(() => {
const unsubscribe = fearState.subscribe(() => { const unsubscribe = fearState.subscribe(() => {
setProgression(fearState.finaleProgression);
setWasCaught(fearState.wasCaught) setWasCaught(fearState.wasCaught)
}); });
return () => unsubscribe(); return () => unsubscribe();
}); }, []);
let elementCount = (FEAR_SETTINGS.EVENT_FINALE_TEXT_COUNT / FEAR_SETTINGS.EVENT_FINALE_DURATION) * progression; useEffect(() => {
if (!wasCaught)
return;
let testElements: Array<JSX.Element> = []; const interval = setInterval(() => {
if (Math.random() > 0.9) return;
for (let x = 0; x < elementCount; x++) const baseText = "the deal has been sealed";
testElements.push(<span className="finale-text" key={x}>the deal has been sealed</span>) 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;
if (wasCaught)
return (<> return (<>
<div className="finale-container"> <div className="finale-container">
{testElements} {elements}
</div> </div>
<div className="scanlines" /> <div className="scanlines" />
</>) </>)
return <></>
} }
+82 -21
View File
@@ -5,6 +5,64 @@ 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";
function ShaderPatch(shader: { vertexShader: string, fragmentShader: string, uniforms: Object }) {
shader.vertexShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
#endif
${shader.vertexShader}
`;
shader.vertexShader = shader.vertexShader.replace(
`#include <project_vertex>`,
`
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
float precisionModifier = 200.0;
gl_Position.xy /= gl_Position.w;
gl_Position.xy = floor(gl_Position.xy * precisionModifier) / precisionModifier;
gl_Position.xy *= gl_Position.w;
vDepth = gl_Position.w;
#ifdef USE_MAP
vAffineUv = vMapUv * gl_Position.w;
#endif
`
);
shader.fragmentShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
#endif
${shader.fragmentShader}
`;
shader.fragmentShader = shader.fragmentShader.replace(
`#include <map_fragment>`,
`
#ifdef USE_MAP
vec2 flatAffineUV = vAffineUv / max(vDepth, 0.001);
vec2 warpDiff = flatAffineUV - vMapUv;
float warpDist = length(warpDiff);
float maxDistortion = 0.25;
float falloff = maxDistortion / (maxDistortion + warpDist);
vec2 finalUV = vMapUv + (warpDiff * falloff);
vec4 texelColor = texture2D( map, finalUV );
diffuseColor *= texelColor;
#endif
`
);
}
interface DoorProps { interface DoorProps {
position: [number, number, number]; position: [number, number, number];
rotation: [number, number, number]; rotation: [number, number, number];
@@ -12,6 +70,7 @@ interface DoorProps {
function Door({ position, rotation }: DoorProps) { function Door({ position, rotation }: DoorProps) {
const [soundUrl, setSoundUrl] = useState<string | null>(null); const [soundUrl, setSoundUrl] = useState<string | null>(null);
const currentSound = useRef<string | null>(null); const currentSound = useRef<string | null>(null);
const steelTex = useTexture('fear/img/steel.png');
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
@@ -33,19 +92,22 @@ function Door({ position, rotation }: DoorProps) {
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 && ( {soundUrl && (
@@ -283,6 +345,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>
@@ -293,12 +356,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>
@@ -308,24 +370,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 && (
@@ -339,12 +399,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 && (
@@ -366,6 +425,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>
))} ))}
@@ -385,6 +445,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
+10 -3
View File
@@ -14,8 +14,9 @@ export const FEAR_SETTINGS = {
EVENT_RUST_LOOP_COUNT: 4, EVENT_RUST_LOOP_COUNT: 4,
EVENT_FINALE_LOOP_COUNT: 5, EVENT_FINALE_LOOP_COUNT: 5,
EVENT_FINALE_DURATION: 3, EVENT_FINALE_DURATION: 1,
EVENT_FINALE_TEXT_COUNT: 128
TEST_MODE: false
}; };
const listeners = new Set<() => void>(); const listeners = new Set<() => void>();
@@ -45,8 +46,14 @@ export const fearState = {
this.currentWidth = newWidth; this.currentWidth = newWidth;
} }
if (this.wasCaught && 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); this.finaleProgression = Math.min(this.finaleProgression + delta, FEAR_SETTINGS.EVENT_FINALE_DURATION);
} else {
window.location.href = '/';
}
}
this.emit(); this.emit();
}, },