Compare commits
16 Commits
8c4080f10c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ab3bf047d4 | |||
| f1ab2b692d | |||
| 673aabce50 | |||
| b3a5712c85 | |||
| 4691a9fbf4 | |||
| 0fca4db440 | |||
| 930139d1df | |||
| 4120e5ec72 | |||
| fd314cf2ec | |||
| e3ab974988 | |||
| e4a0c57e79 | |||
| ebda4b281e | |||
| 8dcc888d5c | |||
| b7e61b4240 | |||
| 23c39a71a6 | |||
| a0ee50703c |
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.
@@ -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,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}
|
||||||
@@ -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,20 +86,20 @@ 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 />
|
||||||
<TheCreature />
|
<TheCreature />
|
||||||
<Player />
|
<Player />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<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
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 = "bwaaaaaaaaa";
|
||||||
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);
|
||||||
|
|
||||||
if (wasCaught)
|
return () => clearInterval(interval);
|
||||||
return (<>
|
}, [wasCaught]);
|
||||||
<div className="finale-container">
|
|
||||||
{testElements}
|
|
||||||
</div>
|
|
||||||
<div className="scanlines" />
|
|
||||||
</>)
|
|
||||||
|
|
||||||
return <></>
|
if (!wasCaught) return null;
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<div className="finale-container">
|
||||||
|
{elements}
|
||||||
|
</div>
|
||||||
|
<div className="scanlines" />
|
||||||
|
</>)
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ 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];
|
||||||
@@ -12,6 +13,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 +35,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 +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>
|
||||||
@@ -293,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>
|
||||||
|
|
||||||
@@ -308,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 && (
|
||||||
@@ -339,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 && (
|
||||||
@@ -366,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>
|
||||||
))}
|
))}
|
||||||
@@ -385,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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
@@ -73,7 +95,7 @@ export default function Player() {
|
|||||||
if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward);
|
if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward);
|
||||||
if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide);
|
if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide);
|
||||||
|
|
||||||
if (targetVelocity.lengthSq() > 0)
|
if (targetVelocity.lengthSq() > 0)
|
||||||
targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED);
|
targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED);
|
||||||
|
|
||||||
currentVelocity.lerp(targetVelocity, 10 * dt);
|
currentVelocity.lerp(targetVelocity, 10 * dt);
|
||||||
@@ -86,15 +108,25 @@ export default function Player() {
|
|||||||
playerRoot.x = THREE.MathUtils.clamp(playerRoot.x, minX, maxX);
|
playerRoot.x = THREE.MathUtils.clamp(playerRoot.x, minX, maxX);
|
||||||
|
|
||||||
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right;
|
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right;
|
||||||
|
|
||||||
bobIntensity.current = THREE.MathUtils.lerp(bobIntensity.current, isMoving ? 1 : 0, 8 * dt);
|
bobIntensity.current = THREE.MathUtils.lerp(bobIntensity.current, isMoving ? 1 : 0, 8 * dt);
|
||||||
|
|
||||||
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
|
||||||
@@ -113,13 +145,13 @@ export default function Player() {
|
|||||||
flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
|
flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
|
||||||
flashlightRef.current.target.updateMatrixWorld();
|
flashlightRef.current.target.updateMatrixWorld();
|
||||||
|
|
||||||
flashlightRef.current.intensity =
|
flashlightRef.current.intensity =
|
||||||
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
|
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
|
||||||
Math.sin(state.clock.elapsedTime * 30) * 0.15 * Math.cos(state.clock.elapsedTime * 3);
|
Math.sin(state.clock.elapsedTime * 30) * 0.15 * Math.cos(state.clock.elapsedTime * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
|
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
|
||||||
const absoluteZ = -playerRoot.z;
|
const absoluteZ = -playerRoot.z;
|
||||||
const rawSegmentIndex = Math.floor(absoluteZ / length);
|
const rawSegmentIndex = Math.floor(absoluteZ / length);
|
||||||
const progressZ = ((absoluteZ % length) + length) % length / 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
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
+11
-4
@@ -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) {
|
||||||
this.finaleProgression = Math.min(this.finaleProgression + delta, FEAR_SETTINGS.EVENT_FINALE_DURATION);
|
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();
|
this.emit();
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user