From eebd87a6508819cac214aebb7ea0d8d212fd34a4 Mon Sep 17 00:00:00 2001 From: neru Date: Fri, 26 Jun 2026 08:48:31 -0300 Subject: [PATCH] style: run format:apply --- src/app/components/discordstatus.css | 114 +-- src/app/components/discordstatus.tsx | 440 +++++----- src/app/fear/page.css | 2 +- src/app/fear/page.tsx | 174 ++-- .../fear/scene-components/ambient-sound.tsx | 99 +-- src/app/fear/scene-components/creature.tsx | 316 +++---- src/app/fear/scene-components/finale-text.css | 124 ++- src/app/fear/scene-components/finale-text.tsx | 112 ++- src/app/fear/scene-components/hallway.tsx | 813 ++++++++++-------- src/app/fear/scene-components/player.tsx | 318 +++---- src/app/fear/shader-patch.ts | 26 +- src/app/fear/state.ts | 118 +-- src/app/niko/page.css | 2 +- src/app/niko/page.tsx | 288 ++++--- .../niko/scene-components/ambient-sound.tsx | 62 +- src/app/niko/scene-components/grass.tsx | 22 +- src/app/niko/scene-components/helpers.tsx | 34 +- src/app/niko/scene-components/sealcube.tsx | 51 +- src/app/niko/scene-components/terrain.tsx | 18 +- src/app/page.css | 39 +- src/app/page.tsx | 168 +++- 21 files changed, 1856 insertions(+), 1484 deletions(-) diff --git a/src/app/components/discordstatus.css b/src/app/components/discordstatus.css index 8a919cb..eecdb7f 100644 --- a/src/app/components/discordstatus.css +++ b/src/app/components/discordstatus.css @@ -1,76 +1,88 @@ .discord-status-compact { - display: flex; - align-items: center; - justify-content: center; - gap: 12px; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; } .avatar-container { - position: relative; - display: inline-block; - width: 34px; - height: 34px; - flex-shrink: 0; + position: relative; + display: inline-block; + width: 34px; + height: 34px; + flex-shrink: 0; } .discord-avatar { - width: 100%; - height: 100%; - border-radius: 2px; - border: 1px solid var(--accent); - box-shadow: 2px 2px 0px var(--pink-accent); - object-fit: cover; + width: 100%; + height: 100%; + border-radius: 2px; + border: 1px solid var(--accent); + box-shadow: 2px 2px 0px var(--pink-accent); + object-fit: cover; } .status-dot { - position: absolute; - bottom: -2px; - right: -2px; - width: 7px; - height: 7px; - border-radius: 50%; - box-shadow: 0 0 0 2px #fffdfd; + position: absolute; + bottom: -2px; + right: -2px; + width: 7px; + height: 7px; + border-radius: 50%; + box-shadow: 0 0 0 2px #fffdfd; +} +.status-dot.online { + background-color: #a7f3d0; + border: 1px solid #34d399; +} +.status-dot.idle { + background-color: #fef08a; + border: 1px solid #facc15; +} +.status-dot.dnd { + background-color: #fecdd3; + border: 1px solid #fb7185; +} +.status-dot.offline { + background-color: var(--text-dim); + border: 1px solid var(--text-main); } -.status-dot.online { background-color: #a7f3d0; border: 1px solid #34d399; } -.status-dot.idle { background-color: #fef08a; border: 1px solid #facc15; } -.status-dot.dnd { background-color: #fecdd3; border: 1px solid #fb7185; } -.status-dot.offline { background-color: var(--text-dim); border: 1px solid var(--text-main); } .status-details { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; } .status-text { - font-size: 0.75rem; - color: var(--text-dim); - text-transform: lowercase; + font-size: 0.75rem; + color: var(--text-dim); + text-transform: lowercase; } .status-text em { - font-style: italic; - font-weight: normal; - color: var(--text-title); + font-style: italic; + font-weight: normal; + color: var(--text-title); } .status-bubble-inline { - font-size: 0.7rem; - color: var(--text-main); - border-bottom: 1px dashed var(--border); - max-width: 220px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + font-size: 0.7rem; + color: var(--text-main); + border-bottom: 1px dashed var(--border); + max-width: 220px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .game-icon { - width: 24px; - height: 24px; - border-radius: 2px; - border: 1px solid var(--border); - box-shadow: 1px 1px 0px var(--pink-accent); - object-fit: cover; - flex-shrink: 0; -} \ No newline at end of file + width: 24px; + height: 24px; + border-radius: 2px; + border: 1px solid var(--border); + box-shadow: 1px 1px 0px var(--pink-accent); + object-fit: cover; + flex-shrink: 0; +} diff --git a/src/app/components/discordstatus.tsx b/src/app/components/discordstatus.tsx index 1bcabe8..df3d1f1 100644 --- a/src/app/components/discordstatus.tsx +++ b/src/app/components/discordstatus.tsx @@ -1,262 +1,284 @@ import './discordstatus.css'; -import { useEffect, useState } from "react"; +import { useEffect, useState } from 'react'; interface LanyardResponse { - success: boolean; - data: LanyardData; + success: boolean; + data: LanyardData; } interface LanyardData { - discord_status: 'online' | 'idle' | 'dnd' | 'offline'; - activities: DiscordActivity[]; - discord_user: DiscordUser; - active_on_discord_web: boolean; - active_on_discord_desktop: boolean; - active_on_discord_mobile: boolean; - active_on_discord_embedded: boolean; - active_on_discord_vr: boolean; - listening_to_spotify: boolean; - spotify: SpotifyData | null; - kv: Record; + discord_status: 'online' | 'idle' | 'dnd' | 'offline'; + activities: DiscordActivity[]; + discord_user: DiscordUser; + active_on_discord_web: boolean; + active_on_discord_desktop: boolean; + active_on_discord_mobile: boolean; + active_on_discord_embedded: boolean; + active_on_discord_vr: boolean; + listening_to_spotify: boolean; + spotify: SpotifyData | null; + kv: Record; } interface SpotifyData { - album: string; - album_art_url: string; - artist: string; - song: string; - track_id: string; - timestamps: { - start: number; - end: number; - }; + album: string; + album_art_url: string; + artist: string; + song: string; + track_id: string; + timestamps: { + start: number; + end: number; + }; } interface DiscordUser { - id: string; - username: string; - discriminator: string; - global_name: string; - display_name: string; - avatar: string; - bot: boolean; - public_flags: number; - avatar_decoration_data: null | any; - collectibles?: { - nameplate?: { - asset: string; - expires_at: string | null; - label: string; - palette: string; - sku_id: string; - }; - }; - display_name_styles?: { - colors: any[]; - effect_id: number; - font_id: number; - }; - primary_guild?: { - badge: null | any; - identity_enabled: boolean; - identity_guild_id: null | string; - tag: null | string; - }; + id: string; + username: string; + discriminator: string; + global_name: string; + display_name: string; + avatar: string; + bot: boolean; + public_flags: number; + avatar_decoration_data: null | any; + collectibles?: { + nameplate?: { + asset: string; + expires_at: string | null; + label: string; + palette: string; + sku_id: string; + }; + }; + display_name_styles?: { + colors: any[]; + effect_id: number; + font_id: number; + }; + primary_guild?: { + badge: null | any; + identity_enabled: boolean; + identity_guild_id: null | string; + tag: null | string; + }; } interface DiscordActivity { - id: string; - name: string; - type: number; - session_id?: string; - created_at: number; - state?: string; - details?: string; - timestamps?: { - start?: number; - end?: number; - }; - assets?: { - large_image?: string; - large_text?: string; - small_image?: string; - small_text?: string; - }; - emoji?: { - name: string; - id?: string; - animated?: boolean; - }; - application_id?: string; - flags?: number; - platform?: string; - sync_id?: string; - content_classification?: { - data: null | any; - loaded: boolean; - }; + id: string; + name: string; + type: number; + session_id?: string; + created_at: number; + state?: string; + details?: string; + timestamps?: { + start?: number; + end?: number; + }; + assets?: { + large_image?: string; + large_text?: string; + small_image?: string; + small_text?: string; + }; + emoji?: { + name: string; + id?: string; + animated?: boolean; + }; + application_id?: string; + flags?: number; + platform?: string; + sync_id?: string; + content_classification?: { + data: null | any; + loaded: boolean; + }; } -function resolveDiscordAsset(applicationId: string | undefined, image: string | undefined): string { - if (!image) return ""; +function resolveDiscordAsset( + applicationId: string | undefined, + image: string | undefined +): string { + if (!image) return ''; - if (image.startsWith("mp:external/")) { - const httpsIndex = image.indexOf("/https/"); - if (httpsIndex !== -1) { - return `https://${image.slice(httpsIndex + "/https/".length)}`; - } - } + if (image.startsWith('mp:external/')) { + const httpsIndex = image.indexOf('/https/'); + if (httpsIndex !== -1) { + return `https://${image.slice(httpsIndex + '/https/'.length)}`; + } + } - if (image.startsWith("spotify:")) - return ""; + if (image.startsWith('spotify:')) return ''; + if (applicationId && image) + return `https://cdn.discordapp.com/app-assets/${applicationId}/${image}.png`; - if (applicationId && image) - return `https://cdn.discordapp.com/app-assets/${applicationId}/${image}.png`; - - return image; + return image; } export interface DiscordStatusParams { - userId: string + userId: string; } const STATUS_LABELS: Record = { - online: 'online', - idle: 'away', - dnd: 'busy', - offline: 'offline' + online: 'online', + idle: 'away', + dnd: 'busy', + offline: 'offline' }; export function DiscordStatus({ userId }: DiscordStatusParams) { - const [presence, setPresence] = useState(null); - const [loading, setLoading] = useState(true); + const [presence, setPresence] = useState(null); + const [loading, setLoading] = useState(true); - useEffect(() => { - let interval: NodeJS.Timeout | null = null; + useEffect(() => { + let interval: NodeJS.Timeout | null = null; - async function fetchRichPresence() { - try { - const response = await fetch(`https://api.lanyard.rest/v1/users/${userId}`); - const json: LanyardResponse = await response.json(); - if (json.success) - setPresence(json.data); - } catch (error) { - console.error("Failed to fetch Lanyard presence:", error); - } finally { - setLoading(false); - } - } + async function fetchRichPresence() { + try { + const response = await fetch( + `https://api.lanyard.rest/v1/users/${userId}` + ); + const json: LanyardResponse = await response.json(); + if (json.success) setPresence(json.data); + } catch (error) { + console.error('Failed to fetch Lanyard presence:', error); + } finally { + setLoading(false); + } + } - const startPolling = () => { - if (!interval) { - fetchRichPresence(); - interval = setInterval(fetchRichPresence, 30000); - } - }; + const startPolling = () => { + if (!interval) { + fetchRichPresence(); + interval = setInterval(fetchRichPresence, 30000); + } + }; - const stopPolling = () => { - if (interval) { - clearInterval(interval); - interval = null; - } - }; + const stopPolling = () => { + if (interval) { + clearInterval(interval); + interval = null; + } + }; - const handleVisibilityChange = () => { - if (document.visibilityState === 'visible') - startPolling(); - else - stopPolling(); + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') startPolling(); + else stopPolling(); + }; - }; + if (document.visibilityState === 'visible') startPolling(); + else setLoading(false); - if (document.visibilityState === 'visible') - startPolling(); - else - setLoading(false); + document.addEventListener('visibilitychange', handleVisibilityChange); - document.addEventListener('visibilitychange', handleVisibilityChange); + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + stopPolling(); + }; + }, [userId]); - return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - stopPolling(); - }; - }, [userId]); + if (loading) + return ( +

+ loading status... +

+ ); - if (loading) - return

loading status...

; + if (!presence) + return ( +

+ offline +

+ ); - if (!presence) - return

offline

; + const customActivity = presence.activities.find((act) => act.id === 'custom'); + const customStatusText = customActivity + ? `${customActivity.emoji?.name || ''} ${customActivity.state || ''}`.trim() + : null; - const customActivity = presence.activities.find(act => act.id === "custom"); - const customStatusText = customActivity - ? `${customActivity.emoji?.name || ''} ${customActivity.state || ''}`.trim() - : null; + const gameActivity = presence.activities + .filter((act) => act.type === 0) + .sort((a, b) => (b.assets ? 1 : 0) - (a.assets ? 1 : 0))[0] as + | DiscordActivity + | undefined; - const gameActivity = presence.activities - .filter(act => act.type === 0) - .sort((a, b) => (b.assets ? 1 : 0) - (a.assets ? 1 : 0))[0] as DiscordActivity | undefined; + const isListeningToSpotify = + presence.listening_to_spotify && presence.spotify; - const isListeningToSpotify = presence.listening_to_spotify && presence.spotify; + let primaryActivity = null; + let activityText = ''; + let activityImage = ''; - let primaryActivity = null; - let activityText = ""; - let activityImage = ""; + if (gameActivity) { + primaryActivity = gameActivity; + activityText = `playing: ${gameActivity.name.toLowerCase()}`; - if (gameActivity) { - primaryActivity = gameActivity; - activityText = `playing: ${gameActivity.name.toLowerCase()}`; + if (gameActivity.details) activityText += ` • ${gameActivity.details}`; - if (gameActivity.details) - activityText += ` • ${gameActivity.details}`; + if (gameActivity.assets) { + const targetImage = + gameActivity.assets.small_image || gameActivity.assets.large_image; + activityImage = resolveDiscordAsset( + gameActivity.application_id, + targetImage + ); + } + } else if (isListeningToSpotify && presence.spotify) { + primaryActivity = { name: 'Spotify' } as DiscordActivity; + activityText = `listening to: ${presence.spotify.song} • ${presence.spotify.artist}`; + activityImage = presence.spotify.album_art_url || ''; + } - if (gameActivity.assets) { - const targetImage = gameActivity.assets.small_image || gameActivity.assets.large_image; - activityImage = resolveDiscordAsset(gameActivity.application_id, targetImage); - } - } - else if (isListeningToSpotify && presence.spotify) { - primaryActivity = { name: "Spotify" } as DiscordActivity; - activityText = `listening to: ${presence.spotify.song} • ${presence.spotify.artist}`; - activityImage = presence.spotify.album_art_url || ""; - } + return ( +
+
+ Discord avatar + +
- return ( -
-
- Discord avatar - -
+
+ + {primaryActivity ? ( + <>{activityText} + ) : ( + <> + currently: {STATUS_LABELS[presence.discord_status]} + + )} + -
- - {primaryActivity ? ( - <>{activityText} - ) : ( - <>currently: {STATUS_LABELS[presence.discord_status]} - )} - + {customStatusText && ( + {customStatusText} + )} +
- {customStatusText && ( - - {customStatusText} - - )} -
- - {activityImage && primaryActivity && ( - {primaryActivity.name} - )} -
- ); + {activityImage && primaryActivity && ( + {primaryActivity.name} + )} +
+ ); } diff --git a/src/app/fear/page.css b/src/app/fear/page.css index 434c512..eeec24e 100644 --- a/src/app/fear/page.css +++ b/src/app/fear/page.css @@ -7,4 +7,4 @@ outline: none; user-select: none; touch-action: none; -} \ No newline at end of file +} diff --git a/src/app/fear/page.tsx b/src/app/fear/page.tsx index 79dc02c..58f286a 100644 --- a/src/app/fear/page.tsx +++ b/src/app/fear/page.tsx @@ -2,9 +2,16 @@ import './page.css'; -import { Canvas, useFrame, useThree } from "@react-three/fiber"; -import { BrightnessContrast, EffectComposer, HueSaturation, Noise, Pixelation, Vignette } from "@react-three/postprocessing"; -import { Suspense, useEffect, useState } from "react"; +import { Canvas, useFrame, useThree } from '@react-three/fiber'; +import { + BrightnessContrast, + EffectComposer, + HueSaturation, + Noise, + Pixelation, + Vignette +} from '@react-three/postprocessing'; +import { Suspense, useEffect, useState } from 'react'; import { AmbientSound } from './scene-components/ambient-sound'; @@ -18,103 +25,112 @@ import { AudioListener } from 'three'; import FinaleText from './scene-components/finale-text'; function PostProcessing() { - const [wasCaught, setWasCaught] = useState(fearState.wasCaught); + const [wasCaught, setWasCaught] = useState(fearState.wasCaught); - useEffect(() => { - const unsubscribe = fearState.subscribe(() => { - setWasCaught(fearState.wasCaught); - }); - return () => unsubscribe(); - }, []); + useEffect(() => { + const unsubscribe = fearState.subscribe(() => { + setWasCaught(fearState.wasCaught); + }); + return () => unsubscribe(); + }, []); - return ( - - - - - - ) + return ( + + + + + + + + ); } function ListenerCreator() { - const { camera } = useThree(); + const { camera } = useThree(); - useEffect(() => { - const listener = new AudioListener(); - camera.add(listener); + useEffect(() => { + const listener = new AudioListener(); + camera.add(listener); - return () => { - camera.remove(listener); - }; - }, [camera]); + return () => { + camera.remove(listener); + }; + }, [camera]); - return null; + return null; } function FearStateUpdater() { - useFrame((state, delta) => { - fearState.update(delta); - }); - return null; + useFrame((state, delta) => { + fearState.update(delta); + }); + return null; } export default function Fear() { - const [isRustActive, setIsRustActive] = useState(fearState.isRustActive); - const [wasCaught, setWasCaught] = useState(fearState.isRustActive); + const [isRustActive, setIsRustActive] = useState(fearState.isRustActive); + const [wasCaught, setWasCaught] = useState(fearState.isRustActive); - useEffect(() => { - const unsubscribe = fearState.subscribe(() => { - setIsRustActive(fearState.isRustActive); - setWasCaught(fearState.wasCaught) - }); - return () => unsubscribe(); - }, []); + useEffect(() => { + const unsubscribe = fearState.subscribe(() => { + setIsRustActive(fearState.isRustActive); + setWasCaught(fearState.wasCaught); + }); + return () => unsubscribe(); + }, []); - return (<> - - + return ( + <> + + - + - + - {FEAR_SETTINGS.TEST_MODE ? : } - {FEAR_SETTINGS.TEST_MODE ? null : } - {FEAR_SETTINGS.TEST_MODE ? null : < PostProcessing />} + {FEAR_SETTINGS.TEST_MODE ? ( + + ) : ( + + )} + {FEAR_SETTINGS.TEST_MODE ? null : ( + + )} + {FEAR_SETTINGS.TEST_MODE ? null : } - - - - - + + + + + - + - + - {wasCaught ? : null} - + {wasCaught ? ( + + ) : null} + - - ) -} \ No newline at end of file + + + ); +} diff --git a/src/app/fear/scene-components/ambient-sound.tsx b/src/app/fear/scene-components/ambient-sound.tsx index dbb458d..f634b87 100644 --- a/src/app/fear/scene-components/ambient-sound.tsx +++ b/src/app/fear/scene-components/ambient-sound.tsx @@ -1,64 +1,67 @@ -import { useEffect, useRef } from 'react' +import { useEffect, useRef } from 'react'; interface AmbientSoundProps { - url: string - volume?: number + url: string; + volume?: number; } export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) { - const audioRef = useRef(null) - const targetVolumeRef = useRef(volume) + const audioRef = useRef(null); + const targetVolumeRef = useRef(volume); - targetVolumeRef.current = volume + targetVolumeRef.current = volume; - useEffect(() => { - const audio = new Audio(url) - audio.loop = true - audio.volume = 0 - audioRef.current = audio + useEffect(() => { + const audio = new Audio(url); + audio.loop = true; + audio.volume = 0; + audioRef.current = audio; - let componentsMounted = true + let componentsMounted = true; - const attemptPlay = () => { - if (!audioRef.current || !componentsMounted) return - - audio.volume = targetVolumeRef.current + const attemptPlay = () => { + if (!audioRef.current || !componentsMounted) return; - if (audio.volume > 0 && audio.paused) { - audio.play().catch((err) => { - console.warn('Autoplay management holding clip playback execution.', err) - }) - } - } + audio.volume = targetVolumeRef.current; - attemptPlay() + if (audio.volume > 0 && audio.paused) { + audio.play().catch((err) => { + console.warn( + 'Autoplay management holding clip playback execution.', + err + ); + }); + } + }; - window.addEventListener('click', attemptPlay) - window.addEventListener('keydown', attemptPlay) + attemptPlay(); - return () => { - componentsMounted = false - window.removeEventListener('click', attemptPlay) - window.removeEventListener('keydown', attemptPlay) - audio.pause() - audio.src = '' - audioRef.current = null - } - }, [url]) + window.addEventListener('click', attemptPlay); + window.addEventListener('keydown', attemptPlay); - useEffect(() => { - const audio = audioRef.current - if (!audio) return + return () => { + componentsMounted = false; + window.removeEventListener('click', attemptPlay); + window.removeEventListener('keydown', attemptPlay); + audio.pause(); + audio.src = ''; + audioRef.current = null; + }; + }, [url]); - if (volume === 0) { - if (!audio.paused) audio.pause() - } else { - audio.volume = volume - if (audio.paused) { - audio.play().catch(() => {}) - } - } - }, [volume]) + useEffect(() => { + const audio = audioRef.current; + if (!audio) return; - return null -} \ No newline at end of file + if (volume === 0) { + if (!audio.paused) audio.pause(); + } else { + audio.volume = volume; + if (audio.paused) { + audio.play().catch(() => {}); + } + } + }, [volume]); + + return null; +} diff --git a/src/app/fear/scene-components/creature.tsx b/src/app/fear/scene-components/creature.tsx index 58ea4c1..b8bb80f 100644 --- a/src/app/fear/scene-components/creature.tsx +++ b/src/app/fear/scene-components/creature.tsx @@ -1,196 +1,196 @@ -import { useTexture, PositionalAudio } from "@react-three/drei"; -import { useFrame, useThree } from "@react-three/fiber"; -import { useEffect, useMemo, useRef, useState } from "react"; +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"; +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 baseTexture = useTexture('fear/img/creature.png'); - const texture = useMemo(() => { - const t = baseTexture.clone(); - t.needsUpdate = true; - return t; - }, [baseTexture]); + 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 meshRef = useRef(null); + const audioRef = useRef(null); + const { camera } = useThree(); - const [hasTriggered, setHasTriggered] = useState(false); - const [isSpawned, setIsSpawned] = useState(false); + const [hasTriggered, setHasTriggered] = useState(false); + const [isSpawned, setIsSpawned] = useState(false); - const globalDistance = useRef(32); - const [finaleTriggered, setFinaleTriggered] = useState(fearState.finaleTriggered); + const globalDistance = useRef(32); + const [finaleTriggered, setFinaleTriggered] = useState( + fearState.finaleTriggered + ); - const audioPlaying = useRef(false); + const audioPlaying = useRef(false); - const movePhase = useRef<'frozen' | 'lurching'>('frozen'); - const phaseTimer = useRef(1.5); + const movePhase = useRef<'frozen' | 'lurching'>('frozen'); + const phaseTimer = useRef(1.5); - const glitchCooldown = useRef(0); - const isGlitchSpiking = useRef(false); - const flickerCooldown = useRef(0); + const glitchCooldown = useRef(0); + const isGlitchSpiking = useRef(false); + const flickerCooldown = useRef(0); - useEffect(() => { - const unsubscribe = fearState.subscribe(() => { - setFinaleTriggered(fearState.finaleTriggered); + 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 (!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(); - }, []); + if (audioRef.current && audioRef.current.isPlaying) + audioRef.current.stop(); + } + }); + return () => unsubscribe(); + }, []); - useFrame((state, delta) => { - if (!fearState.finaleTriggered) return; + useFrame((state, delta) => { + if (!fearState.finaleTriggered) return; - const creature = meshRef.current; - if (!creature) 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 (!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) { + if (globalDistance.current < 40) setHasTriggered(true); + } - } + if (hasTriggered) { + phaseTimer.current -= delta; - 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 (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 (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(); + } - 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; - 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; + } + } - 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 forwardVector = new THREE.Vector3(); - camera.getWorldDirection(forwardVector); - const lookDirZ = forwardVector.z < 0 ? -1 : 1; + const calculatedZ = camera.position.z + lookDirZ * globalDistance.current; - 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); - creature.position.set(0, 1.6, calculatedZ); - creature.lookAt(camera.position.x, creature.position.y, camera.position.z); + if (!hasTriggered) return; - 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; - 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); + } + } - 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; + } - 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); - 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); + } + } - 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 + ); - 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; + } + }); - 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 ( - - - - - {finaleTriggered && ( - - )} - - ); -} \ No newline at end of file + {finaleTriggered && ( + + )} + + ); +} diff --git a/src/app/fear/scene-components/finale-text.css b/src/app/fear/scene-components/finale-text.css index 43a66d4..c675f44 100644 --- a/src/app/fear/scene-components/finale-text.css +++ b/src/app/fear/scene-components/finale-text.css @@ -1,84 +1,82 @@ @font-face { - font-family: 'VCR'; - src: url('/fear/fonts/vcr.ttf') format('truetype'); - font-weight: normal; - font-style: normal; - font-display: swap; + font-family: 'VCR'; + src: url('/fear/fonts/vcr.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-display: swap; } .finale-container { - position: absolute; - width: 100%; - height: 100%; - left: 0; - top: 0vh; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0vh; - display: grid; - align-items: center; - align-content: center; - justify-content: center; - overflow: hidden; + display: grid; + align-items: center; + align-content: center; + justify-content: center; + overflow: hidden; - /* filter: invert(100%); */ - backdrop-filter: brightness(100%); + /* filter: invert(100%); */ + backdrop-filter: brightness(100%); - grid-auto-rows: 5vh; - /* grid-template-columns: 0; */ - grid-template-rows: repeat(auto-fit, max-content); + grid-auto-rows: 5vh; + /* grid-template-columns: 0; */ + grid-template-rows: repeat(auto-fit, max-content); - user-select: none; - - will-change: filter; - animation: invertFlicker 0.07s infinite alternate; + 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); + } - 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%); - } + 44%, + 46%, + 89%, + 93%, + 100% { + filter: invert(100%) contrast(300%) brightness(150%); + backdrop-filter: brightness(30%) hue-rotate(180deg) saturate(500%); + } } .finale-text { - font-family: 'VCR', sans-serif; - font-variant-numeric: tabular-nums; - letter-spacing: 0.1em; + font-family: 'VCR', sans-serif; + font-variant-numeric: tabular-nums; + letter-spacing: 0.1em; - height: 0px; - width: 100%; - color: rgb(255, 255, 255); - font-size: 8vh; - - text-align: center; - white-space: nowrap; + height: 0px; + width: 100%; + color: rgb(255, 255, 255); + font-size: 8vh; + text-align: center; + white-space: nowrap; } .scanlines { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 900; - background: repeating-linear-gradient(rgba(0, 0, 0, 0) 0px, - rgba(0, 0, 0, 0) 2px, - rgba(0, 0, 0, 0.3) 2px, - rgba(0, 0, 0, 0.3) 4px); - -} \ No newline at end of file + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 900; + background: repeating-linear-gradient( + rgba(0, 0, 0, 0) 0px, + rgba(0, 0, 0, 0) 2px, + rgba(0, 0, 0, 0.3) 2px, + rgba(0, 0, 0, 0.3) 4px + ); +} diff --git a/src/app/fear/scene-components/finale-text.tsx b/src/app/fear/scene-components/finale-text.tsx index a91e665..5ce5f4b 100644 --- a/src/app/fear/scene-components/finale-text.tsx +++ b/src/app/fear/scene-components/finale-text.tsx @@ -1,56 +1,86 @@ -import { JSX, useEffect, useState } from "react" -import { fearState } from "../state" +import { JSX, useEffect, useState } from 'react'; +import { fearState } from '../state'; import './finale-text.css'; const BLOCKS = [ - "▀", "▂", "▃", "▄", "▅", "▆", "▇", - "█", "▉", "▊", "▋", "▌", "▍", "▎", "▏", - "▐", "░", "▒", "▓", "▔", "▕", "▖", "▗", - "▘", "▙", "▚", "▛", "▜", "▝", "▞", "▟" + '▀', + '▂', + '▃', + '▄', + '▅', + '▆', + '▇', + '█', + '▉', + '▊', + '▋', + '▌', + '▍', + '▎', + '▏', + '▐', + '░', + '▒', + '▓', + '▔', + '▕', + '▖', + '▗', + '▘', + '▙', + '▚', + '▛', + '▜', + '▝', + '▞', + '▟' ]; export default function FinaleText() { - const [wasCaught, setWasCaught] = useState(fearState.wasCaught); - const [elements, setElements] = useState([]); + const [wasCaught, setWasCaught] = useState(fearState.wasCaught); + const [elements, setElements] = useState([]); - useEffect(() => { - const unsubscribe = fearState.subscribe(() => { - setWasCaught(fearState.wasCaught) - }); - return () => unsubscribe(); - }, []); + useEffect(() => { + const unsubscribe = fearState.subscribe(() => { + setWasCaught(fearState.wasCaught); + }); + return () => unsubscribe(); + }, []); + useEffect(() => { + if (!wasCaught) return; - useEffect(() => { - if (!wasCaught) - return; + const interval = setInterval(() => { + if (Math.random() > 0.9) return; - const interval = setInterval(() => { - if (Math.random() > 0.9) return; + const baseText = 'bwaaaaaaaaa'; + const corrupted = baseText + .split('') + .map((char) => + Math.random() > 0.98 + ? BLOCKS[Math.floor(Math.random() * BLOCKS.length)] + : char + ) + .join(''); - const baseText = "bwaaaaaaaaa"; - const corrupted = baseText - .split("") - .map((char) => (Math.random() > 0.98 ? BLOCKS[Math.floor(Math.random() * BLOCKS.length)] : char)) - .join(""); + setElements((prev) => [ + ...prev.slice(-30), + + {corrupted} + + ]); + }, 10); - setElements((prev) => [...prev.slice(-30), - - {corrupted} - - ]); - }, 10); + return () => clearInterval(interval); + }, [wasCaught]); - return () => clearInterval(interval); - }, [wasCaught]); + if (!wasCaught) return null; - if (!wasCaught) return null; - - return (<> -
- {elements} -
-
- ) -} \ No newline at end of file + return ( + <> +
{elements}
+
+ + ); +} diff --git a/src/app/fear/scene-components/hallway.tsx b/src/app/fear/scene-components/hallway.tsx index 9f97744..12aadcb 100644 --- a/src/app/fear/scene-components/hallway.tsx +++ b/src/app/fear/scene-components/hallway.tsx @@ -1,401 +1,532 @@ -import { useEffect, useRef, useState } from "react"; -import { FEAR_SETTINGS, fearState } from "../state"; -import { useTexture, PositionalAudio } from "@react-three/drei"; +import { useEffect, useRef, useState } from 'react'; +import { FEAR_SETTINGS, fearState } from '../state'; +import { useTexture, PositionalAudio } from '@react-three/drei'; -import * as THREE from "three"; -import { useFrame } from "@react-three/fiber"; -import { ShaderPatch } from "../shader-patch"; +import * as THREE from 'three'; +import { useFrame } from '@react-three/fiber'; +import { ShaderPatch } from '../shader-patch'; interface DoorProps { - position: [number, number, number]; - rotation: [number, number, number]; + position: [number, number, number]; + rotation: [number, number, number]; } function Door({ position, rotation }: DoorProps) { - const [soundUrl, setSoundUrl] = useState(null); - const currentSound = useRef(null); - const steelTex = useTexture('fear/img/steel.png'); + const [soundUrl, setSoundUrl] = useState(null); + const currentSound = useRef(null); + const steelTex = useTexture('fear/img/steel.png'); - useEffect(() => { - const interval = setInterval(() => { - if (Math.random() < 0.02) { - const chosenSound = Math.random() < 0.5 ? "fear/snd/knock1.mp3" : "fear/snd/knock2.mp3"; + useEffect(() => { + const interval = setInterval(() => { + if (Math.random() < 0.02) { + const chosenSound = + Math.random() < 0.5 ? 'fear/snd/knock1.mp3' : 'fear/snd/knock2.mp3'; - setSoundUrl(chosenSound); - currentSound.current = chosenSound; - } - }, 5000); + setSoundUrl(chosenSound); + currentSound.current = chosenSound; + } + }, 5000); - return () => clearInterval(interval); - }, []); + return () => clearInterval(interval); + }, []); - const handleAudioEnded = () => { - setSoundUrl(null); - currentSound.current = null; - }; + const handleAudioEnded = () => { + setSoundUrl(null); + currentSound.current = null; + }; - return ( - - {/* frame */} - - - - + return ( + + {/* frame */} + + + + - {/* panel */} - - - - + {/* panel */} + + + + - {/* handle */} - - - - + {/* handle */} + + + + - {soundUrl && ( - - )} - - ); + {soundUrl && ( + + )} + + ); } export default function Hallway() { - const [width, setWidth] = useState(fearState.currentWidth); - const [floorTex, wallTex, rustWallTex, rustFloorTex] = useTexture([ - 'fear/img/concrete-floor.png', - 'fear/img/concrete-wall.png', - 'fear/img/rust.png', - 'fear/img/rust.png' - ]); + const [width, setWidth] = useState(fearState.currentWidth); + const [floorTex, wallTex, rustWallTex, rustFloorTex] = useTexture([ + 'fear/img/concrete-floor.png', + 'fear/img/concrete-wall.png', + 'fear/img/rust.png', + 'fear/img/rust.png' + ]); - useEffect(() => { - [floorTex, wallTex, rustWallTex, rustFloorTex].forEach((tex) => { - tex.wrapS = tex.wrapT = THREE.RepeatWrapping; - tex.minFilter = tex.magFilter = THREE.NearestFilter; - tex.colorSpace = THREE.SRGBColorSpace; - }); - }, [floorTex, wallTex, rustWallTex, rustFloorTex]); + useEffect(() => { + [floorTex, wallTex, rustWallTex, rustFloorTex].forEach((tex) => { + tex.wrapS = tex.wrapT = THREE.RepeatWrapping; + tex.minFilter = tex.magFilter = THREE.NearestFilter; + tex.colorSpace = THREE.SRGBColorSpace; + }); + }, [floorTex, wallTex, rustWallTex, rustFloorTex]); + const segmentPool = [0, 1, 2, 3, 4]; + const segmentCount = segmentPool.length; - const segmentPool = [0, 1, 2, 3, 4]; - const segmentCount = segmentPool.length; + const lightRefs = useRef<(THREE.PointLight | null)[]>([]); + const matRefs = useRef<(THREE.MeshStandardMaterial | null)[]>([]); - const lightRefs = useRef<(THREE.PointLight | null)[]>([]); - const matRefs = useRef<(THREE.MeshStandardMaterial | null)[]>([]); + const lightState = useRef<'normal' | 'flickering' | 'dead'>('normal'); + const stateEndTime = useRef(0); + const nextEventTime = useRef(5); - const lightState = useRef<'normal' | 'flickering' | 'dead'>('normal'); - const stateEndTime = useRef(0); - const nextEventTime = useRef(5); + const segmentsRef = useRef([]); + const wallMaterialsRef = useRef([]); + const floorMaterialsRef = useRef([]); + const pipeMaterialsRef = useRef([]); + const bracketMaterialsRef = useRef([]); - const segmentsRef = useRef([]); - const wallMaterialsRef = useRef([]); - const floorMaterialsRef = useRef([]); - const pipeMaterialsRef = useRef([]); - const bracketMaterialsRef = useRef([]); + wallMaterialsRef.current = []; + floorMaterialsRef.current = []; + pipeMaterialsRef.current = []; + bracketMaterialsRef.current = []; - wallMaterialsRef.current = []; - floorMaterialsRef.current = []; - pipeMaterialsRef.current = []; - bracketMaterialsRef.current = []; + const [isRustActive, setIsRustActive] = useState(fearState.isRustActive); - const [isRustActive, setIsRustActive] = useState(fearState.isRustActive); + useEffect(() => { + const unsubscribe = fearState.subscribe(() => { + setWidth(fearState.currentWidth); + setIsRustActive(fearState.isRustActive); + }); + return () => unsubscribe(); + }, []); - useEffect(() => { - const unsubscribe = fearState.subscribe(() => { - setWidth(fearState.currentWidth); - setIsRustActive(fearState.isRustActive); - }); - return () => unsubscribe(); - }, []); + useFrame((state, delta) => { + const time = state.clock.elapsedTime; - useFrame((state, delta) => { - const time = state.clock.elapsedTime; - - /* + /* lights */ - let intensity1 = 0.85 + Math.sin(time * 2) * 0.03; - if (time > nextEventTime.current && lightState.current === 'normal') { - lightState.current = 'flickering'; - stateEndTime.current = time + 1.5 + Math.random() * 2; - } - if (lightState.current === 'flickering') { - if (time > stateEndTime.current) { - if (Math.random() > 0.4) { - lightState.current = 'dead'; - stateEndTime.current = time + 1.0 + Math.random() * 2.5; - } else { - lightState.current = 'normal'; - nextEventTime.current = time + 10 + Math.random() * 20; - } - } else { - const baseWave = Math.sin(time * 45) * 0.4 + Math.sin(time * 90) * 0.3; - intensity1 = 0.5 + baseWave; - if (Math.sin(time * 150) + Math.cos(time * 220) > 1.2) intensity1 *= Math.random() > 0.5 ? 0.0 : 0.15; - } - } - if (lightState.current === 'dead') { - if (time > stateEndTime.current) { - lightState.current = 'normal'; - nextEventTime.current = time + 12 + Math.random() * 15; - } else { - intensity1 = Math.random() > 0.98 ? 0.08 : 0.0; - } - } + let intensity1 = 0.85 + Math.sin(time * 2) * 0.03; + if (time > nextEventTime.current && lightState.current === 'normal') { + lightState.current = 'flickering'; + stateEndTime.current = time + 1.5 + Math.random() * 2; + } + if (lightState.current === 'flickering') { + if (time > stateEndTime.current) { + if (Math.random() > 0.4) { + lightState.current = 'dead'; + stateEndTime.current = time + 1.0 + Math.random() * 2.5; + } else { + lightState.current = 'normal'; + nextEventTime.current = time + 10 + Math.random() * 20; + } + } else { + const baseWave = Math.sin(time * 45) * 0.4 + Math.sin(time * 90) * 0.3; + intensity1 = 0.5 + baseWave; + if (Math.sin(time * 150) + Math.cos(time * 220) > 1.2) + intensity1 *= Math.random() > 0.5 ? 0.0 : 0.15; + } + } + if (lightState.current === 'dead') { + if (time > stateEndTime.current) { + lightState.current = 'normal'; + nextEventTime.current = time + 12 + Math.random() * 15; + } else { + intensity1 = Math.random() > 0.98 ? 0.08 : 0.0; + } + } - /* + /* objects */ - const length = FEAR_SETTINGS.HALLWAY_LENGTH; - const playerSegmentZ = Math.floor(state.camera.position.z / length); + const length = FEAR_SETTINGS.HALLWAY_LENGTH; + const playerSegmentZ = Math.floor(state.camera.position.z / length); - const horizontalTexRepeat = width / FEAR_SETTINGS.HALLWAY_WIDTH; - floorTex.repeat.set(horizontalTexRepeat, 10); - wallTex.repeat.set(10, 1); - rustWallTex.repeat.set(10, 1); - rustFloorTex.repeat.set(horizontalTexRepeat, 10); + const horizontalTexRepeat = width / FEAR_SETTINGS.HALLWAY_WIDTH; + floorTex.repeat.set(horizontalTexRepeat, 10); + wallTex.repeat.set(10, 1); + rustWallTex.repeat.set(10, 1); + rustFloorTex.repeat.set(horizontalTexRepeat, 10); - floorTex.needsUpdate = true; - wallTex.needsUpdate = true; - rustWallTex.needsUpdate = true; - rustFloorTex.needsUpdate = true; + floorTex.needsUpdate = true; + wallTex.needsUpdate = true; + rustWallTex.needsUpdate = true; + rustFloorTex.needsUpdate = true; - let closestPoolIndex = 0; - let minDistance = Infinity; + let closestPoolIndex = 0; + let minDistance = Infinity; - segmentsRef.current.forEach((segGroup, poolIndex) => { - if (!segGroup) return; + segmentsRef.current.forEach((segGroup, poolIndex) => { + if (!segGroup) return; - let segmentZIndex = poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ; - segGroup.position.z = segmentZIndex * length; + let segmentZIndex = + poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ; + segGroup.position.z = segmentZIndex * length; - const distance = Math.abs(segGroup.position.z - state.camera.position.z); - if (distance < minDistance) { - minDistance = distance; - closestPoolIndex = poolIndex; - } + const distance = Math.abs(segGroup.position.z - state.camera.position.z); + if (distance < minDistance) { + minDistance = distance; + closestPoolIndex = poolIndex; + } - const leftWallGroup = segGroup.getObjectByName("left-wall-group"); - if (leftWallGroup) leftWallGroup.position.x = -width / 2; + const leftWallGroup = segGroup.getObjectByName('left-wall-group'); + if (leftWallGroup) leftWallGroup.position.x = -width / 2; - const rightWallGroup = segGroup.getObjectByName("right-wall-group"); - if (rightWallGroup) rightWallGroup.position.x = width / 2; + const rightWallGroup = segGroup.getObjectByName('right-wall-group'); + if (rightWallGroup) rightWallGroup.position.x = width / 2; - const floorMesh = segGroup.getObjectByName("floor-mesh"); - if (floorMesh) floorMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH; + const floorMesh = segGroup.getObjectByName('floor-mesh'); + if (floorMesh) floorMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH; - const ceilingMesh = segGroup.getObjectByName("ceiling-mesh"); - if (ceilingMesh) ceilingMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH; + const ceilingMesh = segGroup.getObjectByName('ceiling-mesh'); + if (ceilingMesh) + ceilingMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH; - for (let i = 0; i < 3; i++) { - const pipe = segGroup.getObjectByName(`pipe-${i}`); - if (pipe) pipe.position.x = -width / 2 + 0.4 + (i * 0.20); - } - const bracketGroup = segGroup.getObjectByName("brackets-group"); - if (bracketGroup) { - bracketGroup.children.forEach(b => { - b.position.x = -width / 2 + 0.6; - }); - } - }); + for (let i = 0; i < 3; i++) { + const pipe = segGroup.getObjectByName(`pipe-${i}`); + if (pipe) pipe.position.x = -width / 2 + 0.4 + i * 0.2; + } + const bracketGroup = segGroup.getObjectByName('brackets-group'); + if (bracketGroup) { + bracketGroup.children.forEach((b) => { + b.position.x = -width / 2 + 0.6; + }); + } + }); - /* + /* dyn light */ - segmentPool.forEach((poolIndex) => { - const light = lightRefs.current[poolIndex]; - const mat = matRefs.current[poolIndex]; + segmentPool.forEach((poolIndex) => { + const light = lightRefs.current[poolIndex]; + const mat = matRefs.current[poolIndex]; - if (poolIndex === closestPoolIndex) { - if (light) light.intensity = intensity1 * 1.2; - if (mat) { - mat.emissiveIntensity = intensity1 * 2.5; - if (lightState.current !== 'normal') mat.emissive.setHSL(0.07, 0.4, Math.min(intensity1, 0.7)); - else mat.emissive.setHex(0xa8a1a1); - } - } else { - if (light) light.intensity = 0.9; - if (mat) { - mat.emissiveIntensity = 0.8; - mat.emissive.setHex(0xa8a1a1); - } - } - }); + if (poolIndex === closestPoolIndex) { + if (light) light.intensity = intensity1 * 1.2; + if (mat) { + mat.emissiveIntensity = intensity1 * 2.5; + if (lightState.current !== 'normal') + mat.emissive.setHSL(0.07, 0.4, Math.min(intensity1, 0.7)); + else mat.emissive.setHex(0xa8a1a1); + } + } else { + if (light) light.intensity = 0.9; + if (mat) { + mat.emissiveIntensity = 0.8; + mat.emissive.setHex(0xa8a1a1); + } + } + }); - /* + /* materials */ - 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 => { - if (!mat) return; - const targetTex = isRustActive ? targetRustTex : defaultTex; - if (mat.map !== targetTex) { - mat.map = targetTex; - mat.needsUpdate = true; - } - mat.color.set(isRustActive ? activeColor : defaultColor); - mat.roughness = isRustActive ? activeRough : defaultRough; - mat.metalness = isRustActive ? activeMetal : defaultMetal; - }); - }; + 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) => { + if (!mat) return; + const targetTex = isRustActive ? targetRustTex : defaultTex; + if (mat.map !== targetTex) { + mat.map = targetTex; + mat.needsUpdate = true; + } + mat.color.set(isRustActive ? activeColor : defaultColor); + mat.roughness = isRustActive ? activeRough : defaultRough; + mat.metalness = isRustActive ? activeMetal : defaultMetal; + }); + }; - updateMaterials(wallMaterialsRef.current, wallTex, rustWallTex, "#c5c0be", "#ffffff", 0.95, 0.7, 0.05, 0.1); - updateMaterials(floorMaterialsRef.current, floorTex, rustFloorTex, "#cabdb9", "#ffffff", 0.95, 0.8, 0.05, 0.2); + updateMaterials( + wallMaterialsRef.current, + wallTex, + rustWallTex, + '#c5c0be', + '#ffffff', + 0.95, + 0.7, + 0.05, + 0.1 + ); + updateMaterials( + floorMaterialsRef.current, + floorTex, + rustFloorTex, + '#cabdb9', + '#ffffff', + 0.95, + 0.8, + 0.05, + 0.2 + ); - pipeMaterialsRef.current.forEach(mat => { - if (!mat) return; - mat.color.set(isRustActive ? "#3d1b0f" : "#a5aca8"); - mat.roughness = isRustActive ? 0.95 : 0.0; - mat.metalness = isRustActive ? 0.05 : 0.4; - }); + pipeMaterialsRef.current.forEach((mat) => { + if (!mat) return; + mat.color.set(isRustActive ? '#3d1b0f' : '#a5aca8'); + mat.roughness = isRustActive ? 0.95 : 0.0; + mat.metalness = isRustActive ? 0.05 : 0.4; + }); - bracketMaterialsRef.current.forEach(mat => { - if (!mat) return; - mat.color.set(isRustActive ? "#1b0b05" : "#a5aca8"); - mat.roughness = isRustActive ? 0.95 : 0.0; - mat.metalness = isRustActive ? 0.05 : 0.4; - }); - }); + bracketMaterialsRef.current.forEach((mat) => { + if (!mat) return; + mat.color.set(isRustActive ? '#1b0b05' : '#a5aca8'); + mat.roughness = isRustActive ? 0.95 : 0.0; + mat.metalness = isRustActive ? 0.05 : 0.4; + }); + }); - return ( - <> - {segmentPool.map((poolIndex) => ( - { if (el) segmentsRef.current[poolIndex] = el; }} - position={[0, 0, 0]} - > - {/* lights */} - - { lightRefs.current[poolIndex] = el; }} - intensity={0.9} - distance={15} - color="#a8a1a1" - /> - - - { matRefs.current[poolIndex] = el; }} - color="#111111" - emissive="#a8a1a1" - emissiveIntensity={0.8} - roughness={0.9} - onBeforeCompile={ShaderPatch} - /> - - + return ( + <> + {segmentPool.map((poolIndex) => ( + { + if (el) segmentsRef.current[poolIndex] = el; + }} + position={[0, 0, 0]} + > + {/* lights */} + + { + lightRefs.current[poolIndex] = el; + }} + intensity={0.9} + distance={15} + color='#a8a1a1' + /> + + + { + matRefs.current[poolIndex] = el; + }} + color='#111111' + emissive='#a8a1a1' + emissiveIntensity={0.8} + roughness={0.9} + onBeforeCompile={ShaderPatch} + /> + + - {/* floor */} - - - { if (el) floorMaterialsRef.current.push(el); }} - map={floorTex} - onBeforeCompile={ShaderPatch} - /> - + {/* floor */} + + + { + if (el) floorMaterialsRef.current.push(el); + }} + map={floorTex} + onBeforeCompile={ShaderPatch} + /> + - {/* ceiling */} - - - { if (el) floorMaterialsRef.current.push(el); }} - map={floorTex} - onBeforeCompile={ShaderPatch} - /> - + {/* ceiling */} + + + { + if (el) floorMaterialsRef.current.push(el); + }} + map={floorTex} + onBeforeCompile={ShaderPatch} + /> + - {/* left wall */} - - - - { if (el) wallMaterialsRef.current.push(el); }} - map={wallTex} - onBeforeCompile={ShaderPatch} - /> - - {!isRustActive && ( - <> - - - - )} - + {/* left wall */} + + + + { + if (el) wallMaterialsRef.current.push(el); + }} + map={wallTex} + onBeforeCompile={ShaderPatch} + /> + + {!isRustActive && ( + <> + + + + )} + - {/* right wall */} - - - - { if (el) wallMaterialsRef.current.push(el); }} - map={wallTex} - onBeforeCompile={ShaderPatch} - /> - - {!isRustActive && ( - - )} - + {/* right wall */} + + + + { + if (el) wallMaterialsRef.current.push(el); + }} + map={wallTex} + onBeforeCompile={ShaderPatch} + /> + + {!isRustActive && ( + + )} + - {/* pipes */} - {Array.from({ length: 3 }).map((_, idx) => ( - - - el && pipeMaterialsRef.current.push(el)} - color="#a5aca8" - roughness={0.0} - metalness={0.4} - onBeforeCompile={ShaderPatch} - /> - - ))} + {/* pipes */} + {Array.from({ length: 3 }).map((_, idx) => ( + + + el && pipeMaterialsRef.current.push(el)} + color='#a5aca8' + roughness={0.0} + metalness={0.4} + onBeforeCompile={ShaderPatch} + /> + + ))} - {/* brackets */} - - {Array.from({ length: 5 }).map((_, idx) => { - const zOffset = -(idx * 8 + 4); - return ( - - - el && bracketMaterialsRef.current.push(el)} - color="#a5aca8" - roughness={0.0} - metalness={0.4} - onBeforeCompile={ShaderPatch} - /> - - ); - })} - - - ))} - - ); + {/* brackets */} + + {Array.from({ length: 5 }).map((_, idx) => { + const zOffset = -(idx * 8 + 4); + return ( + + + el && bracketMaterialsRef.current.push(el)} + color='#a5aca8' + roughness={0.0} + metalness={0.4} + onBeforeCompile={ShaderPatch} + /> + + ); + })} + + + ))} + + ); } diff --git a/src/app/fear/scene-components/player.tsx b/src/app/fear/scene-components/player.tsx index 99a62f5..5bf82ec 100644 --- a/src/app/fear/scene-components/player.tsx +++ b/src/app/fear/scene-components/player.tsx @@ -1,8 +1,8 @@ -import { useFrame, useThree } from "@react-three/fiber"; -import { useEffect, useRef } from "react"; -import { FEAR_SETTINGS, fearState } from "../state"; -import { PointerLockControls } from "@react-three/drei"; -import * as THREE from "three"; +import { useFrame, useThree } from '@react-three/fiber'; +import { useEffect, useRef } from 'react'; +import { FEAR_SETTINGS, fearState } from '../state'; +import { PointerLockControls } from '@react-three/drei'; +import * as THREE from 'three'; const forward = new THREE.Vector3(); const side = new THREE.Vector3(); @@ -14,181 +14,209 @@ const targetVelocity = new THREE.Vector3(); const currentVelocity = new THREE.Vector3(); function usePlayerControls() { - const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false }); + const keys = useRef({ + Forward: false, + Backward: false, + Left: false, + Right: false + }); - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = true; - if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = true; - if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = true; - if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = true; - }; + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === 'KeyW' || e.code === 'ArrowUp') + keys.current.Forward = true; + if (e.code === 'KeyS' || e.code === 'ArrowDown') + keys.current.Backward = true; + if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = true; + if (e.code === 'KeyD' || e.code === 'ArrowRight') + keys.current.Right = true; + }; - const handleKeyUp = (e: KeyboardEvent) => { - if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = false; - if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = false; - if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = false; - if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = false; - }; + const handleKeyUp = (e: KeyboardEvent) => { + if (e.code === 'KeyW' || e.code === 'ArrowUp') + keys.current.Forward = false; + if (e.code === 'KeyS' || e.code === 'ArrowDown') + keys.current.Backward = false; + if (e.code === 'KeyA' || e.code === 'ArrowLeft') + keys.current.Left = false; + if (e.code === 'KeyD' || e.code === 'ArrowRight') + keys.current.Right = false; + }; - window.addEventListener('keydown', handleKeyDown); - window.addEventListener('keyup', handleKeyUp); + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); - return () => { - window.removeEventListener('keydown', handleKeyDown); - window.removeEventListener('keyup', handleKeyUp); - }; - }, []); + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + }; + }, []); - return keys.current; + return keys.current; } export default function Player() { - const { camera } = useThree(); - const controls = usePlayerControls(); + const { camera } = useThree(); + const controls = usePlayerControls(); - const flashlightRef = useRef(null); - const movementCounter = useRef(0); - const bobIntensity = useRef(0); + const flashlightRef = useRef(null); + const movementCounter = useRef(0); + const bobIntensity = useRef(0); - const confirmedSegment = useRef(0); - const hasTriggeredThisSegment = useRef(false); + const confirmedSegment = useRef(0); + const hasTriggeredThisSegment = useRef(false); - const footstepAudio = useRef([]); - const hasStepped = useRef(false); + const footstepAudio = useRef([]); + const hasStepped = useRef(false); + useEffect(() => { + 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; - useEffect(() => { - 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 randomIndex = Math.floor( + Math.random() * footstepAudio.current.length + ); + const audio = footstepAudio.current[randomIndex]; - const playRandomFootstep = () => { - if (footstepAudio.current.length === 0) return; + audio.currentTime = 0; + audio.play().catch((err) => { + console.warn( + 'Footstep playback blocked by browser autocomplete/interaction rules.', + err + ); + }); + }; - const randomIndex = Math.floor(Math.random() * footstepAudio.current.length); - const audio = footstepAudio.current[randomIndex]; + useFrame((state, delta) => { + const dt = Math.min(delta, 0.1); - audio.currentTime = 0; - audio.play().catch((err) => { - console.warn("Footstep playback blocked by browser autocomplete/interaction rules.", err); - }); - }; + camera.getWorldDirection(forward); + forward.y = 0; + forward.normalize(); + side.crossVectors(forward, THREE.Object3D.DEFAULT_UP).normalize(); - useFrame((state, delta) => { - const dt = Math.min(delta, 0.1); + const moveForward = Number(controls.Forward) - Number(controls.Backward); + const moveSide = Number(controls.Right) - Number(controls.Left); - camera.getWorldDirection(forward); - forward.y = 0; - forward.normalize(); - side.crossVectors(forward, THREE.Object3D.DEFAULT_UP).normalize(); + targetVelocity.set(0, 0, 0); + if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward); + if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide); - const moveForward = Number(controls.Forward) - Number(controls.Backward); - const moveSide = Number(controls.Right) - Number(controls.Left); + if (targetVelocity.lengthSq() > 0) + targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED); - targetVelocity.set(0, 0, 0); - if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward); - if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide); + currentVelocity.lerp(targetVelocity, 10 * dt); - if (targetVelocity.lengthSq() > 0) - targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED); + playerRoot.x += currentVelocity.x * dt; + playerRoot.z += currentVelocity.z * dt; - currentVelocity.lerp(targetVelocity, 10 * 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); - playerRoot.x += currentVelocity.x * dt; - playerRoot.z += currentVelocity.z * dt; + const isMoving = + controls.Forward || controls.Backward || controls.Left || controls.Right; - 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); + bobIntensity.current = THREE.MathUtils.lerp( + bobIntensity.current, + isMoving ? 1 : 0, + 8 * dt + ); - const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right; + if (isMoving) movementCounter.current += dt * 12; - bobIntensity.current = THREE.MathUtils.lerp(bobIntensity.current, isMoving ? 1 : 0, 8 * dt); + const sinWave = Math.sin(movementCounter.current); + const moveBobY = sinWave * 0.06 * bobIntensity.current; + const moveBobX = + Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current; - if (isMoving) - movementCounter.current += dt * 12; + if (isMoving && sinWave < -0.9) { + if (!hasStepped.current) { + playRandomFootstep(); + hasStepped.current = true; + } + } else if (sinWave > 0) { + hasStepped.current = false; + } - 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 breatheTime = state.clock.elapsedTime * 1.8; + const breatheBobY = + Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5); - if (isMoving && sinWave < -0.9) { - if (!hasStepped.current) { - playRandomFootstep(); - hasStepped.current = true; - } - } else if (sinWave > 0) { - hasStepped.current = false; - } + camera.position.copy(playerRoot); + camera.position.y += moveBobY + breatheBobY; + camera.position.addScaledVector(side, moveBobX); - const breatheTime = state.clock.elapsedTime * 1.8; - const breatheBobY = Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5); + if (flashlightRef.current) { + flashlightRef.current.position.lerp(camera.position, 7 * dt); + camera.getWorldDirection(viewDirection); - camera.position.copy(playerRoot); - camera.position.y += moveBobY + breatheBobY; - camera.position.addScaledVector(side, moveBobX); + targetDest.copy(camera.position).addScaledVector(viewDirection, 10); - if (flashlightRef.current) { - flashlightRef.current.position.lerp(camera.position, 7 * dt); - camera.getWorldDirection(viewDirection); + flashlightRef.current.target.position.lerp(targetDest, 12 * dt); + flashlightRef.current.target.updateMatrixWorld(); - targetDest - .copy(camera.position) - .addScaledVector(viewDirection, 10); + flashlightRef.current.intensity = + FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE + + Math.sin(state.clock.elapsedTime * 30) * + 0.15 * + Math.cos(state.clock.elapsedTime * 3); + } - flashlightRef.current.target.position.lerp(targetDest, 12 * dt); - flashlightRef.current.target.updateMatrixWorld(); + const length = FEAR_SETTINGS.HALLWAY_LENGTH; + const absoluteZ = -playerRoot.z; + const rawSegmentIndex = Math.floor(absoluteZ / length); + const progressZ = (((absoluteZ % length) + length) % length) / length; - flashlightRef.current.intensity = - FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE + - Math.sin(state.clock.elapsedTime * 30) * 0.15 * Math.cos(state.clock.elapsedTime * 3); - } + if (rawSegmentIndex > confirmedSegment.current && progressZ > 0.25) { + if (!hasTriggeredThisSegment.current) { + fearState.registerLoop('forward'); + hasTriggeredThisSegment.current = true; + } + confirmedSegment.current = rawSegmentIndex; + } else if (rawSegmentIndex < confirmedSegment.current && progressZ < 0.75) { + if (!hasTriggeredThisSegment.current) { + fearState.registerLoop('backward'); + hasTriggeredThisSegment.current = true; + } + confirmedSegment.current = rawSegmentIndex; + } - const length = FEAR_SETTINGS.HALLWAY_LENGTH; - const absoluteZ = -playerRoot.z; - const rawSegmentIndex = Math.floor(absoluteZ / length); - const progressZ = ((absoluteZ % length) + length) % length / length; + if ( + rawSegmentIndex === confirmedSegment.current && + progressZ > 0.35 && + progressZ < 0.65 + ) { + hasTriggeredThisSegment.current = false; + } + }); - if (rawSegmentIndex > confirmedSegment.current && progressZ > 0.25) { - if (!hasTriggeredThisSegment.current) { - fearState.registerLoop('forward'); - hasTriggeredThisSegment.current = true; - } - confirmedSegment.current = rawSegmentIndex; - } - else if (rawSegmentIndex < confirmedSegment.current && progressZ < 0.75) { - if (!hasTriggeredThisSegment.current) { - fearState.registerLoop('backward'); - hasTriggeredThisSegment.current = true; - } - confirmedSegment.current = rawSegmentIndex; - } - - if (rawSegmentIndex === confirmedSegment.current && progressZ > 0.35 && progressZ < 0.65) { - hasTriggeredThisSegment.current = false; - } - }); - - return ( - <> - - - - ); -} \ No newline at end of file + return ( + <> + + + + ); +} diff --git a/src/app/fear/shader-patch.ts b/src/app/fear/shader-patch.ts index cda59a5..6e94bde 100644 --- a/src/app/fear/shader-patch.ts +++ b/src/app/fear/shader-patch.ts @@ -1,5 +1,9 @@ -export function ShaderPatch(shader: { vertexShader: string, fragmentShader: string, uniforms: Object }) { - shader.vertexShader = ` +export function ShaderPatch(shader: { + vertexShader: string; + fragmentShader: string; + uniforms: Object; +}) { + shader.vertexShader = ` varying float vDepth; #ifdef USE_MAP varying vec2 vAffineUv; @@ -7,9 +11,9 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri ${shader.vertexShader} `; - shader.vertexShader = shader.vertexShader.replace( - `#include `, - ` + shader.vertexShader = shader.vertexShader.replace( + `#include `, + ` vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); gl_Position = projectionMatrix * mvPosition; @@ -24,9 +28,9 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri vAffineUv = vMapUv * gl_Position.w; #endif ` - ); + ); - shader.fragmentShader = ` + shader.fragmentShader = ` varying float vDepth; #ifdef USE_MAP varying vec2 vAffineUv; @@ -34,9 +38,9 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri ${shader.fragmentShader} `; - shader.fragmentShader = shader.fragmentShader.replace( - `#include `, - ` + shader.fragmentShader = shader.fragmentShader.replace( + `#include `, + ` #ifdef USE_MAP vec2 flatAffineUV = vAffineUv / max(vDepth, 0.001); @@ -53,5 +57,5 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri diffuseColor *= texelColor; #endif ` - ); + ); } diff --git a/src/app/fear/state.ts b/src/app/fear/state.ts index e23c95d..7650270 100644 --- a/src/app/fear/state.ts +++ b/src/app/fear/state.ts @@ -1,74 +1,86 @@ import * as THREE from 'three'; export const FEAR_SETTINGS = { - HALLWAY_LENGTH: 40, - HALLWAY_WIDTH: 6, - HALLWAY_HEIGHT: 5, - PLAYER_HEIGHT: 3, - PLAYER_SPEED: 4, - FLASHLIGHT_INTENSITY_BASE: 8, - WALL_BUFFER: 0.6, - CREATURE_SPEED: 8, + HALLWAY_LENGTH: 40, + HALLWAY_WIDTH: 6, + HALLWAY_HEIGHT: 5, + PLAYER_HEIGHT: 3, + PLAYER_SPEED: 4, + FLASHLIGHT_INTENSITY_BASE: 8, + WALL_BUFFER: 0.6, + CREATURE_SPEED: 8, - EVENT_NARROW_LOOP_COUNT: 2, - EVENT_RUST_LOOP_COUNT: 4, - EVENT_FINALE_LOOP_COUNT: 5, + EVENT_NARROW_LOOP_COUNT: 2, + EVENT_RUST_LOOP_COUNT: 4, + EVENT_FINALE_LOOP_COUNT: 5, - EVENT_FINALE_DURATION: 1, + EVENT_FINALE_DURATION: 1, - TEST_MODE: false + TEST_MODE: false }; const listeners = new Set<() => void>(); export const fearState = { - loopCount: 0, - currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH, - isRustActive: false, - finaleTriggered: false, - wasCaught: false, - finaleProgression: 0, + loopCount: 0, + currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH, + isRustActive: false, + finaleTriggered: false, + wasCaught: false, + finaleProgression: 0, - subscribe(listener: () => void) { - listeners.add(listener); - return () => { listeners.delete(listener); }; - }, + subscribe(listener: () => void) { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, - emit() { - listeners.forEach((listener) => listener()); - }, + emit() { + listeners.forEach((listener) => listener()); + }, - update(delta: number) { - const targetWidth = this.loopCount >= FEAR_SETTINGS.EVENT_NARROW_LOOP_COUNT ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH; - const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta); + update(delta: number) { + const targetWidth = + this.loopCount >= FEAR_SETTINGS.EVENT_NARROW_LOOP_COUNT + ? 2.5 + : FEAR_SETTINGS.HALLWAY_WIDTH; + const newWidth = THREE.MathUtils.lerp( + this.currentWidth, + targetWidth, + 2 * delta + ); - if (Math.abs(this.currentWidth - newWidth) > 0.001) { - this.currentWidth = newWidth; - } + if (Math.abs(this.currentWidth - newWidth) > 0.001) { + this.currentWidth = newWidth; + } - if (this.wasCaught) { - if (this.finaleProgression < FEAR_SETTINGS.EVENT_FINALE_DURATION) { + if (this.wasCaught) { + 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.finaleProgression = Math.min(this.finaleProgression + delta, FEAR_SETTINGS.EVENT_FINALE_DURATION); - } else { - window.location.href = '/'; - } - } + this.emit(); + }, - this.emit(); - }, + registerLoop(direction: 'forward' | 'backward') { + this.loopCount += 1; - registerLoop(direction: 'forward' | 'backward') { - this.loopCount += 1; + this.isRustActive = this.loopCount >= FEAR_SETTINGS.EVENT_RUST_LOOP_COUNT; + this.finaleTriggered = + this.loopCount >= FEAR_SETTINGS.EVENT_FINALE_LOOP_COUNT; - this.isRustActive = this.loopCount >= FEAR_SETTINGS.EVENT_RUST_LOOP_COUNT; - this.finaleTriggered = this.loopCount >= FEAR_SETTINGS.EVENT_FINALE_LOOP_COUNT; + this.emit(); + }, - this.emit(); - }, - - registerCaught() { - this.wasCaught = true; - this.emit(); - } -}; \ No newline at end of file + registerCaught() { + this.wasCaught = true; + this.emit(); + } +}; diff --git a/src/app/niko/page.css b/src/app/niko/page.css index 5cf7dd4..b8879d0 100644 --- a/src/app/niko/page.css +++ b/src/app/niko/page.css @@ -43,4 +43,4 @@ 100% { transform: rotate(360deg); } -} \ No newline at end of file +} diff --git a/src/app/niko/page.tsx b/src/app/niko/page.tsx index 2585089..ccdadac 100644 --- a/src/app/niko/page.tsx +++ b/src/app/niko/page.tsx @@ -2,10 +2,21 @@ import './page.css'; -import { Environment, OrbitControls, useProgress } from "@react-three/drei"; +import { Environment, OrbitControls, useProgress } from '@react-three/drei'; import { Canvas, useLoader } from '@react-three/fiber'; -import { Bloom, BrightnessContrast, DepthOfField, EffectComposer, HueSaturation, LUT, Noise, SMAA, SSAO, Vignette } from '@react-three/postprocessing'; -import { useLayoutEffect, useState } from "react"; +import { + Bloom, + BrightnessContrast, + DepthOfField, + EffectComposer, + HueSaturation, + LUT, + Noise, + SMAA, + SSAO, + Vignette +} from '@react-three/postprocessing'; +import { useLayoutEffect, useState } from 'react'; import { folder, useControls, Leva } from 'leva'; import SealCube from './scene-components/sealcube'; import Terrain from './scene-components/terrain'; @@ -13,161 +24,162 @@ import { LUTCubeLoader } from 'three/examples/jsm/Addons.js'; import { AmbientSound } from './scene-components/ambient-sound'; function Loader() { - const { progress, active } = useProgress(); - const [visible, setVisible] = useState(true); + const { progress, active } = useProgress(); + const [visible, setVisible] = useState(true); - useLayoutEffect(() => { - if (!active && progress === 100) { - const timeout = setTimeout(() => setVisible(false), 500); - return () => clearTimeout(timeout); - } - }, [progress, active]); + useLayoutEffect(() => { + if (!active && progress === 100) { + const timeout = setTimeout(() => setVisible(false), 500); + return () => clearTimeout(timeout); + } + }, [progress, active]); - return ( -
- - - -
- ); + return ( +
+ + + +
+ ); } function Scene() { - const { - terrainDryColor, - terrainLushColor, - chunks, - chunkSize, - resolution, - hillScale, - hillHeight, - detailScale, - detailHeight, - grassDryColor, - grassLushColor, - grassCount, - grassSize, - grassLOD, - grassBlades, - grassSegments, - grassLODStart, - grassLODExponent - } = useControls('Environment', { - Terrain: folder({ - terrainDryColor: '#232a0c', - terrainLushColor: '#142a14', - chunks: { value: 16, min: 4, max: 24, step: 2 }, - chunkSize: { value: 10.0, min: 5.0, max: 40.0, step: 1.0 }, - resolution: { value: 8.0, min: 4.0, max: 30.0, step: 1.0 }, - hillScale: { value: 0.15, min: 0.01, max: 0.5, step: 0.01 }, - hillHeight: { value: 4.0, min: 0.0, max: 20.0, step: 0.5 }, - detailScale: { value: 1.0, min: 0.1, max: 5.0, step: 0.1 }, - detailHeight: { value: 0.3, min: 0.0, max: 2.0, step: 0.05 }, - }), - Grass: folder({ - grassDryColor: '#495a17', - grassLushColor: '#255825', - grassCount: { value: 8000, min: 1000, max: 30000, step: 500 }, - grassSize: { value: 0.85, min: 0.1, max: 2.0, step: 0.05 }, - grassLOD: { value: 60, min: 10, max: 200, step: 5 }, - grassBlades: { value: 3, min: 1, max: 5, step: 1 }, - grassSegments: { value: 4, min: 1, max: 5, step: 1 }, - grassLODStart: { value: 0.15, min: 0.0, max: 0.9, step: 0.05 }, - grassLODExponent: { value: 1.8, min: 0.5, max: 3.0, step: 0.1 }, - }) - }); + const { + terrainDryColor, + terrainLushColor, + chunks, + chunkSize, + resolution, + hillScale, + hillHeight, + detailScale, + detailHeight, + grassDryColor, + grassLushColor, + grassCount, + grassSize, + grassLOD, + grassBlades, + grassSegments, + grassLODStart, + grassLODExponent + } = useControls('Environment', { + Terrain: folder({ + terrainDryColor: '#232a0c', + terrainLushColor: '#142a14', + chunks: { value: 16, min: 4, max: 24, step: 2 }, + chunkSize: { value: 10.0, min: 5.0, max: 40.0, step: 1.0 }, + resolution: { value: 8.0, min: 4.0, max: 30.0, step: 1.0 }, + hillScale: { value: 0.15, min: 0.01, max: 0.5, step: 0.01 }, + hillHeight: { value: 4.0, min: 0.0, max: 20.0, step: 0.5 }, + detailScale: { value: 1.0, min: 0.1, max: 5.0, step: 0.1 }, + detailHeight: { value: 0.3, min: 0.0, max: 2.0, step: 0.05 } + }), + Grass: folder({ + grassDryColor: '#495a17', + grassLushColor: '#255825', + grassCount: { value: 8000, min: 1000, max: 30000, step: 500 }, + grassSize: { value: 0.85, min: 0.1, max: 2.0, step: 0.05 }, + grassLOD: { value: 60, min: 10, max: 200, step: 5 }, + grassBlades: { value: 3, min: 1, max: 5, step: 1 }, + grassSegments: { value: 4, min: 1, max: 5, step: 1 }, + grassLODStart: { value: 0.15, min: 0.0, max: 0.9, step: 0.05 }, + grassLODExponent: { value: 1.8, min: 0.5, max: 3.0, step: 0.1 } + }) + }); - return (<> - + return ( + <> + - + - - + + - + - - ) + + + ); } function LutEffect() { - const lutTexture = useLoader(LUTCubeLoader, 'niko/lut/Landscape6.cube'); - return ; + const lutTexture = useLoader(LUTCubeLoader, 'niko/lut/Landscape6.cube'); + return ; } function PostProcessing() { - return ( - - - - - - - - - ) + return ( + + + + + + + + + + + ); } export default function Seal() { - const isProduction = process.env.NODE_ENV === 'production'; + const isProduction = process.env.NODE_ENV === 'production'; - return ( - <> -