diff --git a/src/app/fear/scene-components/creature.tsx b/src/app/fear/scene-components/creature.tsx index d04305d..58ea4c1 100644 --- a/src/app/fear/scene-components/creature.tsx +++ b/src/app/fear/scene-components/creature.tsx @@ -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(null); const audioRef = useRef(null); const { camera } = useThree(); @@ -21,6 +29,13 @@ export default function TheCreature() { 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); @@ -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; @@ -70,6 +104,7 @@ export default function TheCreature() { camera.position.y += (Math.random() - 0.5) * shakeIntensity; if (globalDistance.current <= 0.1) { + window.location.href = '/'; fearState.registerCaught(); return; } @@ -83,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 ( @@ -91,11 +171,15 @@ export default function TheCreature() { visible={finaleTriggered} > - {finaleTriggered && (