Compare commits
18 Commits
d506071ce2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ab3bf047d4 | |||
| f1ab2b692d | |||
| 673aabce50 | |||
| b3a5712c85 | |||
| 4691a9fbf4 | |||
| 0fca4db440 | |||
| 930139d1df | |||
| 4120e5ec72 | |||
| fd314cf2ec | |||
| e3ab974988 | |||
| e4a0c57e79 | |||
| ebda4b281e | |||
| 8dcc888d5c | |||
| b7e61b4240 | |||
| 23c39a71a6 | |||
| a0ee50703c | |||
| 8c4080f10c | |||
| b9eeed848b |
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
@@ -8,13 +8,14 @@ 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';
|
||||
import Hallway from './scene-components/hallway';
|
||||
|
||||
import { AudioListener } from 'three';
|
||||
import FinaleText from './scene-components/finale-text';
|
||||
|
||||
function PostProcessing() {
|
||||
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
|
||||
@@ -27,9 +28,9 @@ function PostProcessing() {
|
||||
}, []);
|
||||
|
||||
return (<EffectComposer>
|
||||
<Pixelation granularity={wasCaught ? 18 : 12} />
|
||||
<Pixelation granularity={wasCaught ? 18 : 10} />
|
||||
<Vignette />
|
||||
<Noise opacity={wasCaught ? 0.01 : 0.005} />
|
||||
<Noise opacity={wasCaught ? 0.01 : 0.003} />
|
||||
<BrightnessContrast
|
||||
brightness={-0.01}
|
||||
contrast={0.05}
|
||||
@@ -77,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 />
|
||||
|
||||
@@ -85,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 />
|
||||
@@ -98,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
|
||||
@@ -113,5 +114,7 @@ export default function Fear() {
|
||||
volume={1}
|
||||
/> : null}
|
||||
</Canvas>
|
||||
|
||||
<FinaleText />
|
||||
</>)
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
import { useTexture, PositionalAudio } from "@react-three/drei";
|
||||
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 { FEAR_SETTINGS, fearState } from "../state";
|
||||
import { ShaderPatch } from "../shader-patch";
|
||||
|
||||
useTexture.preload('fear/img/creature.png');
|
||||
|
||||
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 audioRef = useRef<THREE.PositionalAudio>(null);
|
||||
const { camera } = useThree();
|
||||
@@ -21,6 +29,13 @@ export default function TheCreature() {
|
||||
|
||||
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(() => {
|
||||
const unsubscribe = fearState.subscribe(() => {
|
||||
setFinaleTriggered(fearState.finaleTriggered);
|
||||
@@ -30,6 +45,8 @@ export default function TheCreature() {
|
||||
setHasTriggered(false);
|
||||
globalDistance.current = 32;
|
||||
audioPlaying.current = false;
|
||||
movePhase.current = 'frozen';
|
||||
phaseTimer.current = 1.5;
|
||||
|
||||
if (audioRef.current && audioRef.current.isPlaying)
|
||||
audioRef.current.stop();
|
||||
@@ -47,16 +64,33 @@ export default function TheCreature() {
|
||||
if (!isSpawned) {
|
||||
setIsSpawned(true);
|
||||
globalDistance.current = 32;
|
||||
movePhase.current = 'frozen';
|
||||
phaseTimer.current = 1.0 + Math.random() * 1.5;
|
||||
}
|
||||
|
||||
if (!hasTriggered) {
|
||||
if (globalDistance.current < 40) {
|
||||
if (globalDistance.current < 40)
|
||||
setHasTriggered(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
audioPlaying.current = true;
|
||||
@@ -84,6 +118,51 @@ export default function TheCreature() {
|
||||
|
||||
creature.position.set(0, 1.6, calculatedZ);
|
||||
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 (
|
||||
@@ -92,11 +171,15 @@ export default function TheCreature() {
|
||||
visible={finaleTriggered}
|
||||
>
|
||||
<planeGeometry args={[3.0, 4.8]} />
|
||||
<meshBasicMaterial
|
||||
<meshStandardMaterial
|
||||
map={texture}
|
||||
transparent={true}
|
||||
depthWrite={false}
|
||||
side={THREE.DoubleSide}
|
||||
onBeforeCompile={ShaderPatch}
|
||||
emissive="#ffffff"
|
||||
emissiveMap={texture}
|
||||
emissiveIntensity={0.15}
|
||||
/>
|
||||
|
||||
{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" />
|
||||
</>)
|
||||
}
|
||||
@@ -1,31 +1,67 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
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 { useFrame } from "@react-three/fiber";
|
||||
import { ShaderPatch } from "../shader-patch";
|
||||
|
||||
interface DoorProps {
|
||||
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');
|
||||
|
||||
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 (
|
||||
<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 && (
|
||||
<PositionalAudio
|
||||
url={soundUrl}
|
||||
distance={25}
|
||||
loop={false}
|
||||
autoplay={true}
|
||||
onEnded={handleAudioEnded}
|
||||
/>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -252,6 +288,7 @@ export default function Hallway() {
|
||||
emissive="#a8a1a1"
|
||||
emissiveIntensity={0.8}
|
||||
roughness={0.9}
|
||||
onBeforeCompile={ShaderPatch}
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
@@ -262,12 +299,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>
|
||||
|
||||
@@ -277,24 +313,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 && (
|
||||
@@ -308,12 +342,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 && (
|
||||
@@ -335,6 +368,7 @@ export default function Hallway() {
|
||||
color="#a5aca8"
|
||||
roughness={0.0}
|
||||
metalness={0.4}
|
||||
onBeforeCompile={ShaderPatch}
|
||||
/>
|
||||
</mesh>
|
||||
))}
|
||||
@@ -354,6 +388,7 @@ export default function Hallway() {
|
||||
color="#a5aca8"
|
||||
roughness={0.0}
|
||||
metalness={0.4}
|
||||
onBeforeCompile={ShaderPatch}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -73,7 +95,7 @@ export default function Player() {
|
||||
if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward);
|
||||
if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide);
|
||||
|
||||
if (targetVelocity.lengthSq() > 0)
|
||||
if (targetVelocity.lengthSq() > 0)
|
||||
targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED);
|
||||
|
||||
currentVelocity.lerp(targetVelocity, 10 * dt);
|
||||
@@ -86,15 +108,25 @@ export default function Player() {
|
||||
playerRoot.x = THREE.MathUtils.clamp(playerRoot.x, minX, maxX);
|
||||
|
||||
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right;
|
||||
|
||||
|
||||
bobIntensity.current = THREE.MathUtils.lerp(bobIntensity.current, isMoving ? 1 : 0, 8 * dt);
|
||||
|
||||
if (isMoving)
|
||||
|
||||
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
|
||||
@@ -113,13 +145,13 @@ export default function Player() {
|
||||
flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
|
||||
flashlightRef.current.target.updateMatrixWorld();
|
||||
|
||||
flashlightRef.current.intensity =
|
||||
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
|
||||
flashlightRef.current.intensity =
|
||||
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
|
||||
Math.sin(state.clock.elapsedTime * 30) * 0.15 * Math.cos(state.clock.elapsedTime * 3);
|
||||
}
|
||||
|
||||
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
|
||||
const absoluteZ = -playerRoot.z;
|
||||
const absoluteZ = -playerRoot.z;
|
||||
const rawSegmentIndex = Math.floor(absoluteZ / length);
|
||||
const progressZ = ((absoluteZ % length) + length) % length / length;
|
||||
|
||||
|
||||
@@ -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
@@ -12,7 +12,11 @@ export const FEAR_SETTINGS = {
|
||||
|
||||
EVENT_NARROW_LOOP_COUNT: 2,
|
||||
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>();
|
||||
@@ -23,6 +27,7 @@ export const fearState = {
|
||||
isRustActive: false,
|
||||
finaleTriggered: false,
|
||||
wasCaught: false,
|
||||
finaleProgression: 0,
|
||||
|
||||
subscribe(listener: () => void) {
|
||||
listeners.add(listener);
|
||||
@@ -39,8 +44,18 @@ export const fearState = {
|
||||
|
||||
if (Math.abs(this.currentWidth - newWidth) > 0.001) {
|
||||
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') {
|
||||
|
||||
Reference in New Issue
Block a user