import { useTexture, PositionalAudio } from "@react-three/drei"; import { useFrame, useThree } from "@react-three/fiber"; 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 baseTexture = useTexture('fear/img/creature.png'); const texture = useMemo(() => { const t = baseTexture.clone(); t.needsUpdate = true; return t; }, [baseTexture]); const meshRef = useRef(null); const audioRef = useRef(null); const { camera } = useThree(); const [hasTriggered, setHasTriggered] = useState(false); const [isSpawned, setIsSpawned] = useState(false); const globalDistance = useRef(32); const [finaleTriggered, setFinaleTriggered] = useState(fearState.finaleTriggered); const audioPlaying = useRef(false); const movePhase = useRef<'frozen' | 'lurching'>('frozen'); const phaseTimer = useRef(1.5); const glitchCooldown = useRef(0); const isGlitchSpiking = useRef(false); const flickerCooldown = useRef(0); useEffect(() => { const unsubscribe = fearState.subscribe(() => { setFinaleTriggered(fearState.finaleTriggered); if (!fearState.finaleTriggered) { setIsSpawned(false); setHasTriggered(false); globalDistance.current = 32; audioPlaying.current = false; movePhase.current = 'frozen'; phaseTimer.current = 1.5; if (audioRef.current && audioRef.current.isPlaying) audioRef.current.stop(); } }); return () => unsubscribe(); }, []); useFrame((state, delta) => { if (!fearState.finaleTriggered) return; const creature = meshRef.current; if (!creature) return; if (!isSpawned) { setIsSpawned(true); globalDistance.current = 32; movePhase.current = 'frozen'; phaseTimer.current = 1.0 + Math.random() * 1.5; } if (!hasTriggered) { if (globalDistance.current < 40) setHasTriggered(true); } if (hasTriggered) { 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; if (audioRef.current.context.state === 'suspended') audioRef.current.context.resume(); audioRef.current.play(); } const shakeIntensity = Math.max(0, 1 - (globalDistance.current / 32)) * 0.22; camera.position.x += (Math.random() - 0.5) * shakeIntensity; camera.position.y += (Math.random() - 0.5) * shakeIntensity; if (globalDistance.current <= 0.1) { window.location.href = '/'; fearState.registerCaught(); return; } } const forwardVector = new THREE.Vector3(); camera.getWorldDirection(forwardVector); const lookDirZ = forwardVector.z < 0 ? -1 : 1; const calculatedZ = camera.position.z + (lookDirZ * globalDistance.current); 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 ( {finaleTriggered && ( )} ); }