feat: make movement jitter, add phase effects

This commit is contained in:
2026-06-02 04:42:07 -03:00
parent 4691a9fbf4
commit b3a5712c85
+90 -6
View File
@@ -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;
@@ -70,6 +104,7 @@ export default function TheCreature() {
camera.position.y += (Math.random() - 0.5) * shakeIntensity; camera.position.y += (Math.random() - 0.5) * shakeIntensity;
if (globalDistance.current <= 0.1) { if (globalDistance.current <= 0.1) {
window.location.href = '/';
fearState.registerCaught(); fearState.registerCaught();
return; return;
} }
@@ -83,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 (
@@ -91,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 && (