Compare commits
14 Commits
eec01440f9
...
6d7651dec9
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d7651dec9 | |||
| ee2eb45527 | |||
| 079986ebec | |||
| a0b416c412 | |||
| c582d6b745 | |||
| beff5e3265 | |||
| c04d8536c0 | |||
| cb15cc3d95 | |||
| 0d72d49d7b | |||
| d66c898f23 | |||
| 566a684bfa | |||
| df81fc1ee0 | |||
| dd5e8a2ae2 | |||
| 20b6a559fd |
Binary file not shown.
@@ -1,4 +1,10 @@
|
|||||||
.canvas {
|
.canvas {
|
||||||
width: 100vw !important;
|
width: 100vw !important;
|
||||||
height: 100vh !important;
|
height: 100vh !important;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
outline: none;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
+31
-13
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import './page.css';
|
import './page.css';
|
||||||
|
|
||||||
import { Canvas, useThree } from "@react-three/fiber";
|
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||||
import { BrightnessContrast, EffectComposer, HueSaturation, Noise, Pixelation, Vignette } from "@react-three/postprocessing";
|
import { BrightnessContrast, EffectComposer, HueSaturation, Noise, Pixelation, Vignette } from "@react-three/postprocessing";
|
||||||
import { Suspense, useEffect, useState } from "react";
|
import { Suspense, useEffect, useState } from "react";
|
||||||
|
|
||||||
@@ -53,6 +53,13 @@ function ListenerCreator() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FearStateUpdater() {
|
||||||
|
useFrame((state, delta) => {
|
||||||
|
fearState.update(delta);
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Fear() {
|
export default function Fear() {
|
||||||
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
|
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
|
||||||
const [wasCaught, setWasCaught] = useState(fearState.isRustActive);
|
const [wasCaught, setWasCaught] = useState(fearState.isRustActive);
|
||||||
@@ -72,10 +79,13 @@ export default function Fear() {
|
|||||||
className='canvas'
|
className='canvas'
|
||||||
camera={{ position: [0, 3, -5], fov: 65, far: 100 }}
|
camera={{ position: [0, 3, -5], fov: 65, far: 100 }}
|
||||||
>
|
>
|
||||||
<ListenerCreator/>
|
<FearStateUpdater />
|
||||||
|
|
||||||
|
<ListenerCreator />
|
||||||
|
|
||||||
<color attach="background" args={['#050505']} />
|
<color attach="background" args={['#050505']} />
|
||||||
|
|
||||||
|
<ambientLight intensity={0.0225} />
|
||||||
<fogExp2 attach='fog' args={[0x050505, 0.035]} />
|
<fogExp2 attach='fog' args={[0x050505, 0.035]} />
|
||||||
<PostProcessing />
|
<PostProcessing />
|
||||||
|
|
||||||
@@ -83,17 +93,25 @@ export default function Fear() {
|
|||||||
<Hallway />
|
<Hallway />
|
||||||
<TheCreature />
|
<TheCreature />
|
||||||
<Player />
|
<Player />
|
||||||
|
|
||||||
<AmbientSound
|
|
||||||
url='fear/snd/ambience.mp3'
|
|
||||||
volume={isRustActive ? 0 : 1}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{wasCaught ? <AmbientSound
|
|
||||||
url='fear/snd/glitch.mp3'
|
|
||||||
volume={1}
|
|
||||||
/> : null}
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
|
<AmbientSound
|
||||||
|
key="ambient-1"
|
||||||
|
url='fear/snd/ambience.mp3'
|
||||||
|
volume={isRustActive ? 0 : 1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AmbientSound
|
||||||
|
key="ambient-2"
|
||||||
|
url='fear/snd/ambience2.mp3'
|
||||||
|
volume={isRustActive ? 1 : 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{wasCaught ? <AmbientSound
|
||||||
|
key="ambient-glitch"
|
||||||
|
url='fear/snd/glitch.mp3'
|
||||||
|
volume={1}
|
||||||
|
/> : null}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
@@ -7,13 +7,39 @@ interface AmbientSoundProps {
|
|||||||
|
|
||||||
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
|
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||||
|
const targetVolumeRef = useRef<number>(volume)
|
||||||
|
|
||||||
|
targetVolumeRef.current = volume
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audio = new Audio(url)
|
const audio = new Audio(url)
|
||||||
audio.loop = true
|
audio.loop = true
|
||||||
|
audio.volume = 0
|
||||||
audioRef.current = audio
|
audioRef.current = audio
|
||||||
|
|
||||||
|
let componentsMounted = true
|
||||||
|
|
||||||
|
const attemptPlay = () => {
|
||||||
|
if (!audioRef.current || !componentsMounted) return
|
||||||
|
|
||||||
|
audio.volume = targetVolumeRef.current
|
||||||
|
|
||||||
|
if (audio.volume > 0 && audio.paused) {
|
||||||
|
audio.play().catch((err) => {
|
||||||
|
console.warn('Autoplay management holding clip playback execution.', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptPlay()
|
||||||
|
|
||||||
|
window.addEventListener('click', attemptPlay)
|
||||||
|
window.addEventListener('keydown', attemptPlay)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
componentsMounted = false
|
||||||
|
window.removeEventListener('click', attemptPlay)
|
||||||
|
window.removeEventListener('keydown', attemptPlay)
|
||||||
audio.pause()
|
audio.pause()
|
||||||
audio.src = ''
|
audio.src = ''
|
||||||
audioRef.current = null
|
audioRef.current = null
|
||||||
@@ -24,30 +50,15 @@ export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
|
|||||||
const audio = audioRef.current
|
const audio = audioRef.current
|
||||||
if (!audio) return
|
if (!audio) return
|
||||||
|
|
||||||
audio.volume = volume
|
if (volume === 0) {
|
||||||
|
if (!audio.paused) audio.pause()
|
||||||
const startAudio = () => {
|
} else {
|
||||||
if (audio.volume > 0) {
|
audio.volume = volume
|
||||||
audio.play().catch((err) => {
|
if (audio.paused) {
|
||||||
console.warn('Autoplay blocked. Waiting for user interaction.', err)
|
audio.play().catch(() => {})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [volume])
|
||||||
if (volume === 0) {
|
|
||||||
audio.pause()
|
|
||||||
} else {
|
|
||||||
startAudio()
|
|
||||||
|
|
||||||
window.addEventListener('click', startAudio, { once: true })
|
|
||||||
window.addEventListener('keydown', startAudio, { once: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('click', startAudio)
|
|
||||||
window.removeEventListener('keydown', startAudio)
|
|
||||||
}
|
|
||||||
}, [volume])
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,9 @@ import { useFrame, useThree } from "@react-three/fiber";
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { fearState } from "../state";
|
import { FEAR_SETTINGS, fearState } from "../state";
|
||||||
|
|
||||||
|
useTexture.preload('fear/img/creature.png');
|
||||||
|
|
||||||
export default function TheCreature() {
|
export default function TheCreature() {
|
||||||
const texture = useTexture('fear/img/creature.png');
|
const texture = useTexture('fear/img/creature.png');
|
||||||
@@ -14,32 +16,30 @@ export default function TheCreature() {
|
|||||||
const [hasTriggered, setHasTriggered] = useState(false);
|
const [hasTriggered, setHasTriggered] = useState(false);
|
||||||
const [isSpawned, setIsSpawned] = useState(false);
|
const [isSpawned, setIsSpawned] = useState(false);
|
||||||
|
|
||||||
const speed = 15;
|
|
||||||
const globalDistance = useRef<number>(32);
|
const globalDistance = useRef<number>(32);
|
||||||
const [currentLoop, setCurrentLoop] = useState(fearState.loopCount);
|
const [finaleTriggered, setFinaleTriggered] = useState(fearState.finaleTriggered);
|
||||||
|
|
||||||
const audioPlaying = useRef<boolean>(false);
|
const audioPlaying = useRef<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = fearState.subscribe(() => {
|
const unsubscribe = fearState.subscribe(() => {
|
||||||
setCurrentLoop(fearState.loopCount);
|
setFinaleTriggered(fearState.finaleTriggered);
|
||||||
|
|
||||||
if (fearState.loopCount < 4) {
|
if (!fearState.finaleTriggered) {
|
||||||
setIsSpawned(false);
|
setIsSpawned(false);
|
||||||
setHasTriggered(false);
|
setHasTriggered(false);
|
||||||
globalDistance.current = 32;
|
globalDistance.current = 32;
|
||||||
audioPlaying.current = false;
|
audioPlaying.current = false;
|
||||||
|
|
||||||
if (audioRef.current && audioRef.current.isPlaying) {
|
if (audioRef.current && audioRef.current.isPlaying)
|
||||||
audioRef.current.stop();
|
audioRef.current.stop();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useFrame((state, delta) => {
|
useFrame((state, delta) => {
|
||||||
if (fearState.loopCount < 4) return;
|
if (!fearState.finaleTriggered) return;
|
||||||
|
|
||||||
const creature = meshRef.current;
|
const creature = meshRef.current;
|
||||||
if (!creature) return;
|
if (!creature) return;
|
||||||
@@ -56,7 +56,7 @@ export default function TheCreature() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasTriggered) {
|
if (hasTriggered) {
|
||||||
globalDistance.current -= speed * delta;
|
globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * delta;
|
||||||
|
|
||||||
if (audioRef.current && !audioPlaying.current) {
|
if (audioRef.current && !audioPlaying.current) {
|
||||||
audioPlaying.current = true;
|
audioPlaying.current = true;
|
||||||
@@ -89,7 +89,7 @@ export default function TheCreature() {
|
|||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
ref={meshRef}
|
ref={meshRef}
|
||||||
visible={currentLoop >= 4}
|
visible={finaleTriggered}
|
||||||
>
|
>
|
||||||
<planeGeometry args={[3.0, 4.8]} />
|
<planeGeometry args={[3.0, 4.8]} />
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
@@ -99,7 +99,7 @@ export default function TheCreature() {
|
|||||||
side={THREE.DoubleSide}
|
side={THREE.DoubleSide}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{currentLoop >= 4 && (
|
{finaleTriggered && (
|
||||||
<PositionalAudio
|
<PositionalAudio
|
||||||
url="fear/snd/riser.mp3"
|
url="fear/snd/riser.mp3"
|
||||||
ref={audioRef}
|
ref={audioRef}
|
||||||
|
|||||||
@@ -32,19 +32,21 @@ function Door({ position, rotation }: DoorProps) {
|
|||||||
|
|
||||||
export default function Hallway() {
|
export default function Hallway() {
|
||||||
const [width, setWidth] = useState(fearState.currentWidth);
|
const [width, setWidth] = useState(fearState.currentWidth);
|
||||||
const [floorTex, wallTex, rustTex] = useTexture([
|
const [floorTex, wallTex, rustWallTex, rustFloorTex] = useTexture([
|
||||||
'fear/img/concrete-floor.png',
|
'fear/img/concrete-floor.png',
|
||||||
'fear/img/concrete-wall.png',
|
'fear/img/concrete-wall.png',
|
||||||
|
'fear/img/rust.png',
|
||||||
'fear/img/rust.png'
|
'fear/img/rust.png'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
[floorTex, wallTex, rustTex].forEach((tex) => {
|
[floorTex, wallTex, rustWallTex, rustFloorTex].forEach((tex) => {
|
||||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||||
tex.minFilter = tex.magFilter = THREE.NearestFilter;
|
tex.minFilter = tex.magFilter = THREE.NearestFilter;
|
||||||
tex.colorSpace = THREE.SRGBColorSpace;
|
tex.colorSpace = THREE.SRGBColorSpace;
|
||||||
});
|
});
|
||||||
}, [floorTex, wallTex, rustTex]);
|
}, [floorTex, wallTex, rustWallTex, rustFloorTex]);
|
||||||
|
|
||||||
|
|
||||||
const segmentPool = [0, 1, 2, 3, 4];
|
const segmentPool = [0, 1, 2, 3, 4];
|
||||||
const segmentCount = segmentPool.length;
|
const segmentCount = segmentPool.length;
|
||||||
@@ -80,10 +82,6 @@ export default function Hallway() {
|
|||||||
useFrame((state, delta) => {
|
useFrame((state, delta) => {
|
||||||
const time = state.clock.elapsedTime;
|
const time = state.clock.elapsedTime;
|
||||||
|
|
||||||
fearState.update(delta);
|
|
||||||
if (fearState.isRustActive !== isRustActive)
|
|
||||||
setIsRustActive(fearState.isRustActive);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
lights
|
lights
|
||||||
*/
|
*/
|
||||||
@@ -125,11 +123,13 @@ export default function Hallway() {
|
|||||||
const horizontalTexRepeat = width / FEAR_SETTINGS.HALLWAY_WIDTH;
|
const horizontalTexRepeat = width / FEAR_SETTINGS.HALLWAY_WIDTH;
|
||||||
floorTex.repeat.set(horizontalTexRepeat, 10);
|
floorTex.repeat.set(horizontalTexRepeat, 10);
|
||||||
wallTex.repeat.set(10, 1);
|
wallTex.repeat.set(10, 1);
|
||||||
rustTex.repeat.set(horizontalTexRepeat, 10);
|
rustWallTex.repeat.set(10, 1);
|
||||||
|
rustFloorTex.repeat.set(horizontalTexRepeat, 10);
|
||||||
|
|
||||||
floorTex.needsUpdate = true;
|
floorTex.needsUpdate = true;
|
||||||
wallTex.needsUpdate = true;
|
wallTex.needsUpdate = true;
|
||||||
rustTex.needsUpdate = true;
|
rustWallTex.needsUpdate = true;
|
||||||
|
rustFloorTex.needsUpdate = true;
|
||||||
|
|
||||||
let closestPoolIndex = 0;
|
let closestPoolIndex = 0;
|
||||||
let minDistance = Infinity;
|
let minDistance = Infinity;
|
||||||
@@ -140,7 +140,6 @@ export default function Hallway() {
|
|||||||
let segmentZIndex = poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ;
|
let segmentZIndex = poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ;
|
||||||
segGroup.position.z = segmentZIndex * length;
|
segGroup.position.z = segmentZIndex * length;
|
||||||
|
|
||||||
// Track which pool index is currently physically closest to the player's camera position
|
|
||||||
const distance = Math.abs(segGroup.position.z - state.camera.position.z);
|
const distance = Math.abs(segGroup.position.z - state.camera.position.z);
|
||||||
if (distance < minDistance) {
|
if (distance < minDistance) {
|
||||||
minDistance = distance;
|
minDistance = distance;
|
||||||
@@ -197,10 +196,10 @@ export default function Hallway() {
|
|||||||
/*
|
/*
|
||||||
materials
|
materials
|
||||||
*/
|
*/
|
||||||
const updateMaterials = (materials: THREE.MeshStandardMaterial[], defaultTex: THREE.Texture, activeColor: string, defaultColor: string, activeRough: number, defaultRough: number, activeMetal: number, defaultMetal: number) => {
|
const updateMaterials = (materials: THREE.MeshStandardMaterial[], defaultTex: THREE.Texture, targetRustTex: THREE.Texture, activeColor: string, defaultColor: string, activeRough: number, defaultRough: number, activeMetal: number, defaultMetal: number) => {
|
||||||
materials.forEach(mat => {
|
materials.forEach(mat => {
|
||||||
if (!mat) return;
|
if (!mat) return;
|
||||||
const targetTex = isRustActive ? rustTex : defaultTex;
|
const targetTex = isRustActive ? targetRustTex : defaultTex;
|
||||||
if (mat.map !== targetTex) {
|
if (mat.map !== targetTex) {
|
||||||
mat.map = targetTex;
|
mat.map = targetTex;
|
||||||
mat.needsUpdate = true;
|
mat.needsUpdate = true;
|
||||||
@@ -211,8 +210,8 @@ export default function Hallway() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
updateMaterials(wallMaterialsRef.current, wallTex, "#918a87", "#ffffff", 0.95, 0.7, 0.05, 0.1);
|
updateMaterials(wallMaterialsRef.current, wallTex, rustWallTex, "#c5c0be", "#ffffff", 0.95, 0.7, 0.05, 0.1);
|
||||||
updateMaterials(floorMaterialsRef.current, floorTex, "#8b827f", "#ffffff", 0.95, 0.8, 0.05, 0.2);
|
updateMaterials(floorMaterialsRef.current, floorTex, rustFloorTex, "#cabdb9", "#ffffff", 0.95, 0.8, 0.05, 0.2);
|
||||||
|
|
||||||
pipeMaterialsRef.current.forEach(mat => {
|
pipeMaterialsRef.current.forEach(mat => {
|
||||||
if (!mat) return;
|
if (!mat) return;
|
||||||
@@ -231,8 +230,6 @@ export default function Hallway() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ambientLight intensity={0.0225} />
|
|
||||||
|
|
||||||
{segmentPool.map((poolIndex) => (
|
{segmentPool.map((poolIndex) => (
|
||||||
<group
|
<group
|
||||||
key={poolIndex}
|
key={poolIndex}
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import { useFrame, useThree } from "@react-three/fiber";
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { FEAR_SETTINGS, fearState } from "../state";
|
import { FEAR_SETTINGS, fearState } from "../state";
|
||||||
import { PointerLockControls } from "@react-three/drei";
|
import { PointerLockControls } from "@react-three/drei";
|
||||||
|
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
|
||||||
const forward = new THREE.Vector3();
|
const forward = new THREE.Vector3();
|
||||||
const side = new THREE.Vector3();
|
const side = new THREE.Vector3();
|
||||||
const direction = new THREE.Vector3();
|
|
||||||
const viewDirection = new THREE.Vector3();
|
const viewDirection = new THREE.Vector3();
|
||||||
|
const targetDest = new THREE.Vector3();
|
||||||
|
|
||||||
|
const playerRoot = new THREE.Vector3(0, FEAR_SETTINGS.PLAYER_HEIGHT, 0);
|
||||||
|
const targetVelocity = new THREE.Vector3();
|
||||||
|
const currentVelocity = new THREE.Vector3();
|
||||||
|
|
||||||
function usePlayerControls() {
|
function usePlayerControls() {
|
||||||
const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false });
|
const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false });
|
||||||
@@ -46,63 +49,77 @@ export default function Player() {
|
|||||||
|
|
||||||
const flashlightRef = useRef<THREE.SpotLight>(null);
|
const flashlightRef = useRef<THREE.SpotLight>(null);
|
||||||
const movementCounter = useRef<number>(0);
|
const movementCounter = useRef<number>(0);
|
||||||
|
const bobIntensity = useRef<number>(0);
|
||||||
|
|
||||||
const confirmedSegment = useRef<number>(0);
|
const confirmedSegment = useRef<number>(0);
|
||||||
const hasTriggeredThisSegment = useRef<boolean>(false);
|
const hasTriggeredThisSegment = useRef<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
playerRoot.set(camera.position.x, FEAR_SETTINGS.PLAYER_HEIGHT, camera.position.z);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useFrame((state, delta) => {
|
useFrame((state, delta) => {
|
||||||
|
const dt = Math.min(delta, 0.1);
|
||||||
|
|
||||||
camera.getWorldDirection(forward);
|
camera.getWorldDirection(forward);
|
||||||
forward.y = 0;
|
forward.y = 0;
|
||||||
forward.normalize();
|
forward.normalize();
|
||||||
|
side.crossVectors(forward, THREE.Object3D.DEFAULT_UP).normalize();
|
||||||
side.crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
|
|
||||||
|
|
||||||
const moveForward = Number(controls.Forward) - Number(controls.Backward);
|
const moveForward = Number(controls.Forward) - Number(controls.Backward);
|
||||||
const moveSide = Number(controls.Right) - Number(controls.Left);
|
const moveSide = Number(controls.Right) - Number(controls.Left);
|
||||||
|
|
||||||
direction.set(0, 0, 0);
|
targetVelocity.set(0, 0, 0);
|
||||||
if (moveForward !== 0) direction.addScaledVector(forward, moveForward);
|
if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward);
|
||||||
if (moveSide !== 0) direction.addScaledVector(side, moveSide);
|
if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide);
|
||||||
|
|
||||||
if (direction.lengthSq() > 0)
|
if (targetVelocity.lengthSq() > 0)
|
||||||
direction.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED * delta);
|
targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED);
|
||||||
|
|
||||||
camera.position.x += direction.x;
|
currentVelocity.lerp(targetVelocity, 10 * dt);
|
||||||
camera.position.z += direction.z;
|
|
||||||
|
playerRoot.x += currentVelocity.x * dt;
|
||||||
|
playerRoot.z += currentVelocity.z * dt;
|
||||||
|
|
||||||
|
const minX = -fearState.currentWidth / 2 + FEAR_SETTINGS.WALL_BUFFER;
|
||||||
|
const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER;
|
||||||
|
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;
|
||||||
if (isMoving) {
|
|
||||||
movementCounter.current += delta * 10;
|
bobIntensity.current = THREE.MathUtils.lerp(bobIntensity.current, isMoving ? 1 : 0, 8 * dt);
|
||||||
camera.position.y = FEAR_SETTINGS.PLAYER_HEIGHT + Math.sin(movementCounter.current) * 0.08;
|
|
||||||
camera.position.x += Math.cos(movementCounter.current / 2) * 0.006;
|
if (isMoving)
|
||||||
} else {
|
movementCounter.current += dt * 12;
|
||||||
const breatheTime = state.clock.elapsedTime * 1.5;
|
|
||||||
const breatheY = FEAR_SETTINGS.PLAYER_HEIGHT + Math.sin(breatheTime) * 0.1;
|
const moveBobY = Math.sin(movementCounter.current) * 0.06 * bobIntensity.current;
|
||||||
camera.position.y = THREE.MathUtils.lerp(camera.position.y, breatheY, 4 * delta);
|
const moveBobX = Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current;
|
||||||
}
|
|
||||||
|
const breatheTime = state.clock.elapsedTime * 1.8;
|
||||||
|
const breatheBobY = Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5);
|
||||||
|
|
||||||
|
camera.position.copy(playerRoot);
|
||||||
|
camera.position.y += moveBobY + breatheBobY;
|
||||||
|
camera.position.addScaledVector(side, moveBobX);
|
||||||
|
|
||||||
if (flashlightRef.current) {
|
if (flashlightRef.current) {
|
||||||
flashlightRef.current.position.copy(camera.position);
|
flashlightRef.current.position.copy(camera.position);
|
||||||
camera.getWorldDirection(viewDirection);
|
camera.getWorldDirection(viewDirection);
|
||||||
|
|
||||||
const targetDest = new THREE.Vector3()
|
targetDest
|
||||||
.copy(camera.position)
|
.copy(camera.position)
|
||||||
.addScaledVector(viewDirection, 10);
|
.addScaledVector(viewDirection, 10);
|
||||||
|
|
||||||
flashlightRef.current.target.position.lerp(targetDest, 10 * delta);
|
flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
|
||||||
flashlightRef.current.target.updateMatrixWorld();
|
flashlightRef.current.target.updateMatrixWorld();
|
||||||
|
|
||||||
flashlightRef.current.intensity = 5 + Math.sin(state.clock.elapsedTime * 30) * 0.3;
|
flashlightRef.current.intensity =
|
||||||
|
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
|
||||||
|
Math.sin(state.clock.elapsedTime * 30) * 0.15 * Math.cos(state.clock.elapsedTime * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
const minX = -fearState.currentWidth / 2 + FEAR_SETTINGS.WALL_BUFFER;
|
|
||||||
const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER;
|
|
||||||
|
|
||||||
if (camera.position.x < minX) camera.position.x = minX;
|
|
||||||
if (camera.position.x > maxX) camera.position.x = maxX;
|
|
||||||
|
|
||||||
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
|
const length = FEAR_SETTINGS.HALLWAY_LENGTH;
|
||||||
const absoluteZ = -camera.position.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;
|
||||||
|
|
||||||
@@ -121,8 +138,9 @@ export default function Player() {
|
|||||||
confirmedSegment.current = rawSegmentIndex;
|
confirmedSegment.current = rawSegmentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawSegmentIndex === confirmedSegment.current && progressZ > 0.35 && progressZ < 0.65)
|
if (rawSegmentIndex === confirmedSegment.current && progressZ > 0.35 && progressZ < 0.65) {
|
||||||
hasTriggeredThisSegment.current = false;
|
hasTriggeredThisSegment.current = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -130,12 +148,15 @@ export default function Player() {
|
|||||||
<PointerLockControls />
|
<PointerLockControls />
|
||||||
<spotLight
|
<spotLight
|
||||||
ref={flashlightRef}
|
ref={flashlightRef}
|
||||||
distance={22}
|
distance={25}
|
||||||
angle={0.35}
|
angle={0.35}
|
||||||
penumbra={0.7}
|
penumbra={0.8}
|
||||||
intensity={5}
|
intensity={0}
|
||||||
color="#fffaed"
|
color="#fffaed"
|
||||||
|
decay={2}
|
||||||
|
castShadow
|
||||||
|
shadow-bias={-0.001}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+11
-4
@@ -6,7 +6,13 @@ export const FEAR_SETTINGS = {
|
|||||||
HALLWAY_HEIGHT: 5,
|
HALLWAY_HEIGHT: 5,
|
||||||
PLAYER_HEIGHT: 3,
|
PLAYER_HEIGHT: 3,
|
||||||
PLAYER_SPEED: 4,
|
PLAYER_SPEED: 4,
|
||||||
|
FLASHLIGHT_INTENSITY_BASE: 8,
|
||||||
WALL_BUFFER: 0.6,
|
WALL_BUFFER: 0.6,
|
||||||
|
CREATURE_SPEED: 8,
|
||||||
|
|
||||||
|
EVENT_NARROW_LOOP_COUNT: 2,
|
||||||
|
EVENT_RUST_LOOP_COUNT: 4,
|
||||||
|
EVENT_FINALE_LOOP_COUNT: 5
|
||||||
};
|
};
|
||||||
|
|
||||||
const listeners = new Set<() => void>();
|
const listeners = new Set<() => void>();
|
||||||
@@ -28,10 +34,7 @@ export const fearState = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
update(delta: number) {
|
update(delta: number) {
|
||||||
this.isRustActive = this.loopCount >= 3;
|
const targetWidth = this.loopCount >= FEAR_SETTINGS.EVENT_NARROW_LOOP_COUNT ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH;
|
||||||
this.finaleTriggered = this.loopCount >= 4;
|
|
||||||
|
|
||||||
const targetWidth = this.loopCount >= 2 ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH;
|
|
||||||
const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta);
|
const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta);
|
||||||
|
|
||||||
if (Math.abs(this.currentWidth - newWidth) > 0.001) {
|
if (Math.abs(this.currentWidth - newWidth) > 0.001) {
|
||||||
@@ -42,6 +45,10 @@ export const fearState = {
|
|||||||
|
|
||||||
registerLoop(direction: 'forward' | 'backward') {
|
registerLoop(direction: 'forward' | 'backward') {
|
||||||
this.loopCount += 1;
|
this.loopCount += 1;
|
||||||
|
|
||||||
|
this.isRustActive = this.loopCount >= FEAR_SETTINGS.EVENT_RUST_LOOP_COUNT;
|
||||||
|
this.finaleTriggered = this.loopCount >= FEAR_SETTINGS.EVENT_FINALE_LOOP_COUNT;
|
||||||
|
|
||||||
this.emit();
|
this.emit();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user