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 { fearState } from './state';
import { FEAR_SETTINGS, fearState } from './state';
import TheCreature from './scene-components/creature';
import Player from './scene-components/player';
@@ -28,7 +28,7 @@ function PostProcessing() {
}, []);
return (<EffectComposer>
<Pixelation granularity={wasCaught ? 18 : 12} />
<Pixelation granularity={wasCaught ? 18 : 10} />
<Vignette />
<Noise opacity={wasCaught ? 0.01 : 0.005} />
<BrightnessContrast
@@ -78,7 +78,7 @@ export default function Fear() {
shadows
gl={{ antialias: true }}
className='canvas'
camera={{ position: [0, 3, -5], fov: 65, far: 100 }}
camera={{ position: [0, 3, -5], fov: 55, far: 100 }}
>
<FearStateUpdater />
@@ -86,9 +86,9 @@ export default function Fear() {
<color attach="background" args={['#050505']} />
<ambientLight intensity={0.0225} />
<fogExp2 attach='fog' args={[0x050505, 0.035]} />
<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 />
@@ -99,7 +99,7 @@ export default function Fear() {
<AmbientSound
key="ambient-1"
url='fear/snd/ambience.mp3'
volume={isRustActive ? 0 : 1}
volume={isRustActive ? 0 : 0.5}
/>
<AmbientSound
@@ -70,7 +70,6 @@ export default function TheCreature() {
camera.position.y += (Math.random() - 0.5) * shakeIntensity;
if (globalDistance.current <= 0.1) {
window.location.href = '/';
fearState.registerCaught();
return;
}
+34 -2
View File
@@ -19,22 +19,54 @@
justify-content: center;
overflow: hidden;
grid-auto-rows: 2.5vh;
/* 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: 5vh;
font-size: 8vh;
text-align: center;
white-space: nowrap;
}
.scanlines {
+33 -13
View File
@@ -1,36 +1,56 @@
import { JSX, useEffect, useState } from "react"
import { FEAR_SETTINGS, fearState } from "../state"
import { fearState } from "../state"
import './finale-text.css';
const BLOCKS = [
"▀", "▂", "▃", "▄", "▅", "▆", "▇",
"█", "▉", "▊", "▋", "▌", "▍", "▎", "▏",
"▐", "░", "▒", "▓", "▔", "▕", "▖", "▗",
"▘", "▙", "▚", "▛", "▜", "▝", "▞", "▟"
];
export default function FinaleText() {
const [progression, setProgression] = useState(fearState.finaleProgression);
const [wasCaught, setWasCaught] = useState(fearState.isRustActive);
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
const [elements, setElements] = useState<JSX.Element[]>([]);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setProgression(fearState.finaleProgression);
setWasCaught(fearState.wasCaught)
});
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++)
testElements.push(<span className="finale-text" key={x}>the deal has been sealed</span>)
const baseText = "the deal has been sealed";
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 (<>
<div className="finale-container">
{testElements}
{elements}
</div>
<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 { useFrame } from "@react-three/fiber";
function ShaderPatch(shader: { vertexShader: string, fragmentShader: string, uniforms: Object }) {
shader.vertexShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
#endif
${shader.vertexShader}
`;
shader.vertexShader = shader.vertexShader.replace(
`#include <project_vertex>`,
`
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
float precisionModifier = 200.0;
gl_Position.xy /= gl_Position.w;
gl_Position.xy = floor(gl_Position.xy * precisionModifier) / precisionModifier;
gl_Position.xy *= gl_Position.w;
vDepth = gl_Position.w;
#ifdef USE_MAP
vAffineUv = vMapUv * gl_Position.w;
#endif
`
);
shader.fragmentShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
#endif
${shader.fragmentShader}
`;
shader.fragmentShader = shader.fragmentShader.replace(
`#include <map_fragment>`,
`
#ifdef USE_MAP
vec2 flatAffineUV = vAffineUv / max(vDepth, 0.001);
vec2 warpDiff = flatAffineUV - vMapUv;
float warpDist = length(warpDiff);
float maxDistortion = 0.25;
float falloff = maxDistortion / (maxDistortion + warpDist);
vec2 finalUV = vMapUv + (warpDiff * falloff);
vec4 texelColor = texture2D( map, finalUV );
diffuseColor *= texelColor;
#endif
`
);
}
interface DoorProps {
position: [number, number, number];
rotation: [number, number, number];
@@ -12,6 +70,7 @@ interface 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(() => {
@@ -33,19 +92,22 @@ function Door({ position, rotation }: DoorProps) {
return (
<group position={position} rotation={rotation}>
<mesh position={[0, 2, -0.14]}>
<boxGeometry args={[2.4, 4.0, 0.2]} />
<meshStandardMaterial color="#8a8585" roughness={0.8} metalness={0.2} />
{/* 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>
<mesh position={[0, 1.95, -0.08]}>
<boxGeometry args={[2.1, 3.8, 0.1]} />
<meshStandardMaterial color="#4e4b4b" roughness={0.7} metalness={0.2} />
{/* 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>
<mesh position={[0.9, 1.8, 0.08]}>
<boxGeometry args={[0.08, 0.08, 0.15]} />
<meshStandardMaterial color="#4e4b4b" roughness={0.4} metalness={0.2} />
{/* handle */}
<mesh position={[0.75, 1.8, .085]}>
<boxGeometry args={[0.3, 0.08, 0.1]} />
<meshStandardMaterial map={steelTex} color="#ffffff" onBeforeCompile={ShaderPatch} />
</mesh>
{soundUrl && (
@@ -283,6 +345,7 @@ export default function Hallway() {
emissive="#a8a1a1"
emissiveIntensity={0.8}
roughness={0.9}
onBeforeCompile={ShaderPatch}
/>
</mesh>
</group>
@@ -293,12 +356,11 @@ export default function Hallway() {
rotation={[-Math.PI / 2, 0, 0]}
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
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
map={floorTex}
roughness={0.8}
metalness={0.2}
onBeforeCompile={ShaderPatch}
/>
</mesh>
@@ -308,24 +370,22 @@ export default function Hallway() {
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]} />
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH, 4, 10]} />
<meshStandardMaterial
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
map={floorTex}
roughness={0.8}
metalness={0.2}
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]} />
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT, 10, 4]} />
<meshStandardMaterial
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
map={wallTex}
roughness={0.7}
metalness={0.1}
onBeforeCompile={ShaderPatch}
/>
</mesh>
{!isRustActive && (
@@ -339,12 +399,11 @@ export default function Hallway() {
{/* 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]} />
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT, 10, 4]} />
<meshStandardMaterial
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
map={wallTex}
roughness={0.7}
metalness={0.1}
onBeforeCompile={ShaderPatch}
/>
</mesh>
{!isRustActive && (
@@ -366,6 +425,7 @@ export default function Hallway() {
color="#a5aca8"
roughness={0.0}
metalness={0.4}
onBeforeCompile={ShaderPatch}
/>
</mesh>
))}
@@ -385,6 +445,7 @@ export default function Hallway() {
color="#a5aca8"
roughness={0.0}
metalness={0.4}
onBeforeCompile={ShaderPatch}
/>
</mesh>
);
+34 -2
View File
@@ -54,10 +54,32 @@ export default function Player() {
const confirmedSegment = useRef<number>(0);
const hasTriggeredThisSegment = 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;
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) => {
const dt = Math.min(delta, 0.1);
@@ -92,9 +114,19 @@ export default function Player() {
if (isMoving)
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;
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 breatheBobY = Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5);
@@ -103,7 +135,7 @@ export default function Player() {
camera.position.addScaledVector(side, moveBobX);
if (flashlightRef.current) {
flashlightRef.current.position.copy(camera.position);
flashlightRef.current.position.lerp(camera.position, 7 * dt);
camera.getWorldDirection(viewDirection);
targetDest
+10 -3
View File
@@ -14,8 +14,9 @@ export const FEAR_SETTINGS = {
EVENT_RUST_LOOP_COUNT: 4,
EVENT_FINALE_LOOP_COUNT: 5,
EVENT_FINALE_DURATION: 3,
EVENT_FINALE_TEXT_COUNT: 128
EVENT_FINALE_DURATION: 1,
TEST_MODE: false
};
const listeners = new Set<() => void>();
@@ -45,8 +46,14 @@ export const fearState = {
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);
} else {
window.location.href = '/';
}
}
this.emit();
},