style: run format:apply

This commit is contained in:
2026-06-26 08:48:31 -03:00
parent ab3bf047d4
commit eebd87a650
21 changed files with 1856 additions and 1484 deletions
+63 -51
View File
@@ -1,76 +1,88 @@
.discord-status-compact { .discord-status-compact {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12px; gap: 12px;
} }
.avatar-container { .avatar-container {
position: relative; position: relative;
display: inline-block; display: inline-block;
width: 34px; width: 34px;
height: 34px; height: 34px;
flex-shrink: 0; flex-shrink: 0;
} }
.discord-avatar { .discord-avatar {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 2px; border-radius: 2px;
border: 1px solid var(--accent); border: 1px solid var(--accent);
box-shadow: 2px 2px 0px var(--pink-accent); box-shadow: 2px 2px 0px var(--pink-accent);
object-fit: cover; object-fit: cover;
} }
.status-dot { .status-dot {
position: absolute; position: absolute;
bottom: -2px; bottom: -2px;
right: -2px; right: -2px;
width: 7px; width: 7px;
height: 7px; height: 7px;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 0 0 2px #fffdfd; 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 { .status-details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 2px; gap: 2px;
} }
.status-text { .status-text {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-dim); color: var(--text-dim);
text-transform: lowercase; text-transform: lowercase;
} }
.status-text em { .status-text em {
font-style: italic; font-style: italic;
font-weight: normal; font-weight: normal;
color: var(--text-title); color: var(--text-title);
} }
.status-bubble-inline { .status-bubble-inline {
font-size: 0.7rem; font-size: 0.7rem;
color: var(--text-main); color: var(--text-main);
border-bottom: 1px dashed var(--border); border-bottom: 1px dashed var(--border);
max-width: 220px; max-width: 220px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.game-icon { .game-icon {
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 2px; border-radius: 2px;
border: 1px solid var(--border); border: 1px solid var(--border);
box-shadow: 1px 1px 0px var(--pink-accent); box-shadow: 1px 1px 0px var(--pink-accent);
object-fit: cover; object-fit: cover;
flex-shrink: 0; flex-shrink: 0;
} }
+231 -209
View File
@@ -1,262 +1,284 @@
import './discordstatus.css'; import './discordstatus.css';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
interface LanyardResponse { interface LanyardResponse {
success: boolean; success: boolean;
data: LanyardData; data: LanyardData;
} }
interface LanyardData { interface LanyardData {
discord_status: 'online' | 'idle' | 'dnd' | 'offline'; discord_status: 'online' | 'idle' | 'dnd' | 'offline';
activities: DiscordActivity[]; activities: DiscordActivity[];
discord_user: DiscordUser; discord_user: DiscordUser;
active_on_discord_web: boolean; active_on_discord_web: boolean;
active_on_discord_desktop: boolean; active_on_discord_desktop: boolean;
active_on_discord_mobile: boolean; active_on_discord_mobile: boolean;
active_on_discord_embedded: boolean; active_on_discord_embedded: boolean;
active_on_discord_vr: boolean; active_on_discord_vr: boolean;
listening_to_spotify: boolean; listening_to_spotify: boolean;
spotify: SpotifyData | null; spotify: SpotifyData | null;
kv: Record<string, string>; kv: Record<string, string>;
} }
interface SpotifyData { interface SpotifyData {
album: string; album: string;
album_art_url: string; album_art_url: string;
artist: string; artist: string;
song: string; song: string;
track_id: string; track_id: string;
timestamps: { timestamps: {
start: number; start: number;
end: number; end: number;
}; };
} }
interface DiscordUser { interface DiscordUser {
id: string; id: string;
username: string; username: string;
discriminator: string; discriminator: string;
global_name: string; global_name: string;
display_name: string; display_name: string;
avatar: string; avatar: string;
bot: boolean; bot: boolean;
public_flags: number; public_flags: number;
avatar_decoration_data: null | any; avatar_decoration_data: null | any;
collectibles?: { collectibles?: {
nameplate?: { nameplate?: {
asset: string; asset: string;
expires_at: string | null; expires_at: string | null;
label: string; label: string;
palette: string; palette: string;
sku_id: string; sku_id: string;
}; };
}; };
display_name_styles?: { display_name_styles?: {
colors: any[]; colors: any[];
effect_id: number; effect_id: number;
font_id: number; font_id: number;
}; };
primary_guild?: { primary_guild?: {
badge: null | any; badge: null | any;
identity_enabled: boolean; identity_enabled: boolean;
identity_guild_id: null | string; identity_guild_id: null | string;
tag: null | string; tag: null | string;
}; };
} }
interface DiscordActivity { interface DiscordActivity {
id: string; id: string;
name: string; name: string;
type: number; type: number;
session_id?: string; session_id?: string;
created_at: number; created_at: number;
state?: string; state?: string;
details?: string; details?: string;
timestamps?: { timestamps?: {
start?: number; start?: number;
end?: number; end?: number;
}; };
assets?: { assets?: {
large_image?: string; large_image?: string;
large_text?: string; large_text?: string;
small_image?: string; small_image?: string;
small_text?: string; small_text?: string;
}; };
emoji?: { emoji?: {
name: string; name: string;
id?: string; id?: string;
animated?: boolean; animated?: boolean;
}; };
application_id?: string; application_id?: string;
flags?: number; flags?: number;
platform?: string; platform?: string;
sync_id?: string; sync_id?: string;
content_classification?: { content_classification?: {
data: null | any; data: null | any;
loaded: boolean; loaded: boolean;
}; };
} }
function resolveDiscordAsset(applicationId: string | undefined, image: string | undefined): string { function resolveDiscordAsset(
if (!image) return ""; applicationId: string | undefined,
image: string | undefined
): string {
if (!image) return '';
if (image.startsWith("mp:external/")) { if (image.startsWith('mp:external/')) {
const httpsIndex = image.indexOf("/https/"); const httpsIndex = image.indexOf('/https/');
if (httpsIndex !== -1) { if (httpsIndex !== -1) {
return `https://${image.slice(httpsIndex + "/https/".length)}`; return `https://${image.slice(httpsIndex + '/https/'.length)}`;
} }
} }
if (image.startsWith("spotify:")) if (image.startsWith('spotify:')) return '';
return "";
if (applicationId && image)
return `https://cdn.discordapp.com/app-assets/${applicationId}/${image}.png`;
if (applicationId && image) return image;
return `https://cdn.discordapp.com/app-assets/${applicationId}/${image}.png`;
return image;
} }
export interface DiscordStatusParams { export interface DiscordStatusParams {
userId: string userId: string;
} }
const STATUS_LABELS: Record<LanyardData['discord_status'], string> = { const STATUS_LABELS: Record<LanyardData['discord_status'], string> = {
online: 'online', online: 'online',
idle: 'away', idle: 'away',
dnd: 'busy', dnd: 'busy',
offline: 'offline' offline: 'offline'
}; };
export function DiscordStatus({ userId }: DiscordStatusParams) { export function DiscordStatus({ userId }: DiscordStatusParams) {
const [presence, setPresence] = useState<LanyardData | null>(null); const [presence, setPresence] = useState<LanyardData | null>(null);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
useEffect(() => { useEffect(() => {
let interval: NodeJS.Timeout | null = null; let interval: NodeJS.Timeout | null = null;
async function fetchRichPresence() { async function fetchRichPresence() {
try { try {
const response = await fetch(`https://api.lanyard.rest/v1/users/${userId}`); const response = await fetch(
const json: LanyardResponse = await response.json(); `https://api.lanyard.rest/v1/users/${userId}`
if (json.success) );
setPresence(json.data); const json: LanyardResponse = await response.json();
} catch (error) { if (json.success) setPresence(json.data);
console.error("Failed to fetch Lanyard presence:", error); } catch (error) {
} finally { console.error('Failed to fetch Lanyard presence:', error);
setLoading(false); } finally {
} setLoading(false);
} }
}
const startPolling = () => { const startPolling = () => {
if (!interval) { if (!interval) {
fetchRichPresence(); fetchRichPresence();
interval = setInterval(fetchRichPresence, 30000); interval = setInterval(fetchRichPresence, 30000);
} }
}; };
const stopPolling = () => { const stopPolling = () => {
if (interval) { if (interval) {
clearInterval(interval); clearInterval(interval);
interval = null; interval = null;
} }
}; };
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') if (document.visibilityState === 'visible') startPolling();
startPolling(); else stopPolling();
else };
stopPolling();
}; if (document.visibilityState === 'visible') startPolling();
else setLoading(false);
if (document.visibilityState === 'visible') document.addEventListener('visibilitychange', handleVisibilityChange);
startPolling();
else
setLoading(false);
document.addEventListener('visibilitychange', handleVisibilityChange); return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
stopPolling();
};
}, [userId]);
return () => { if (loading)
document.removeEventListener('visibilitychange', handleVisibilityChange); return (
stopPolling(); <p
}; style={{
}, [userId]); fontSize: '0.75rem',
fontStyle: 'italic',
color: 'var(--text-dim)'
}}
>
loading status...
</p>
);
if (loading) if (!presence)
return <p style={{ fontSize: '0.75rem', fontStyle: 'italic', color: 'var(--text-dim)' }}>loading status...</p>; return (
<p
style={{
fontSize: '0.75rem',
fontStyle: 'italic',
color: 'var(--text-dim)'
}}
>
offline
</p>
);
if (!presence) const customActivity = presence.activities.find((act) => act.id === 'custom');
return <p style={{ fontSize: '0.75rem', fontStyle: 'italic', color: 'var(--text-dim)' }}>offline</p>; const customStatusText = customActivity
? `${customActivity.emoji?.name || ''} ${customActivity.state || ''}`.trim()
: null;
const customActivity = presence.activities.find(act => act.id === "custom"); const gameActivity = presence.activities
const customStatusText = customActivity .filter((act) => act.type === 0)
? `${customActivity.emoji?.name || ''} ${customActivity.state || ''}`.trim() .sort((a, b) => (b.assets ? 1 : 0) - (a.assets ? 1 : 0))[0] as
: null; | DiscordActivity
| undefined;
const gameActivity = presence.activities const isListeningToSpotify =
.filter(act => act.type === 0) presence.listening_to_spotify && presence.spotify;
.sort((a, b) => (b.assets ? 1 : 0) - (a.assets ? 1 : 0))[0] as DiscordActivity | undefined;
const isListeningToSpotify = presence.listening_to_spotify && presence.spotify; let primaryActivity = null;
let activityText = '';
let activityImage = '';
let primaryActivity = null; if (gameActivity) {
let activityText = ""; primaryActivity = gameActivity;
let activityImage = ""; activityText = `playing: ${gameActivity.name.toLowerCase()}`;
if (gameActivity) { if (gameActivity.details) activityText += `${gameActivity.details}`;
primaryActivity = gameActivity;
activityText = `playing: ${gameActivity.name.toLowerCase()}`;
if (gameActivity.details) if (gameActivity.assets) {
activityText += `${gameActivity.details}`; 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) { return (
const targetImage = gameActivity.assets.small_image || gameActivity.assets.large_image; <div className='discord-status-compact'>
activityImage = resolveDiscordAsset(gameActivity.application_id, targetImage); <div className='avatar-container'>
} <img
} src={`https://api.lanyard.rest/${userId}.png`}
else if (isListeningToSpotify && presence.spotify) { alt='Discord avatar'
primaryActivity = { name: "Spotify" } as DiscordActivity; className='discord-avatar'
activityText = `listening to: ${presence.spotify.song}${presence.spotify.artist}`; />
activityImage = presence.spotify.album_art_url || ""; <span className={`status-dot ${presence.discord_status}`} />
} </div>
return ( <div className='status-details'>
<div className="discord-status-compact"> <span className='status-text'>
<div className="avatar-container"> {primaryActivity ? (
<img <>{activityText}</>
src={`https://api.lanyard.rest/${userId}.png`} ) : (
alt="Discord avatar" <>
className="discord-avatar" currently: <em>{STATUS_LABELS[presence.discord_status]}</em>
/> </>
<span className={`status-dot ${presence.discord_status}`} /> )}
</div> </span>
<div className="status-details"> {customStatusText && (
<span className="status-text"> <span className='status-bubble-inline'>{customStatusText}</span>
{primaryActivity ? ( )}
<>{activityText}</> </div>
) : (
<>currently: <em>{STATUS_LABELS[presence.discord_status]}</em></>
)}
</span>
{customStatusText && ( {activityImage && primaryActivity && (
<span className="status-bubble-inline"> <img
{customStatusText} src={activityImage}
</span> alt={primaryActivity.name}
)} className='game-icon'
</div> />
)}
{activityImage && primaryActivity && ( </div>
<img );
src={activityImage}
alt={primaryActivity.name}
className="game-icon"
/>
)}
</div>
);
} }
+1 -1
View File
@@ -7,4 +7,4 @@
outline: none; outline: none;
user-select: none; user-select: none;
touch-action: none; touch-action: none;
} }
+95 -79
View File
@@ -2,9 +2,16 @@
import './page.css'; import './page.css';
import { Canvas, useFrame, useThree } from "@react-three/fiber"; import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { BrightnessContrast, EffectComposer, HueSaturation, Noise, Pixelation, Vignette } from "@react-three/postprocessing"; import {
import { Suspense, useEffect, useState } from "react"; BrightnessContrast,
EffectComposer,
HueSaturation,
Noise,
Pixelation,
Vignette
} from '@react-three/postprocessing';
import { Suspense, useEffect, useState } from 'react';
import { AmbientSound } from './scene-components/ambient-sound'; import { AmbientSound } from './scene-components/ambient-sound';
@@ -18,103 +25,112 @@ import { AudioListener } from 'three';
import FinaleText from './scene-components/finale-text'; import FinaleText from './scene-components/finale-text';
function PostProcessing() { function PostProcessing() {
const [wasCaught, setWasCaught] = useState(fearState.wasCaught); const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
useEffect(() => { useEffect(() => {
const unsubscribe = fearState.subscribe(() => { const unsubscribe = fearState.subscribe(() => {
setWasCaught(fearState.wasCaught); setWasCaught(fearState.wasCaught);
}); });
return () => unsubscribe(); return () => unsubscribe();
}, []); }, []);
return (<EffectComposer> return (
<Pixelation granularity={wasCaught ? 18 : 10} /> <EffectComposer>
<Vignette /> <Pixelation granularity={wasCaught ? 18 : 10} />
<Noise opacity={wasCaught ? 0.01 : 0.003} /> <Vignette />
<BrightnessContrast <Noise opacity={wasCaught ? 0.01 : 0.003} />
brightness={-0.01} <BrightnessContrast brightness={-0.01} contrast={0.05} />
contrast={0.05} <HueSaturation saturation={wasCaught ? 1 : 0} />
/> </EffectComposer>
<HueSaturation saturation={wasCaught ? 1 : 0} /> );
</EffectComposer>)
} }
function ListenerCreator() { function ListenerCreator() {
const { camera } = useThree(); const { camera } = useThree();
useEffect(() => { useEffect(() => {
const listener = new AudioListener(); const listener = new AudioListener();
camera.add(listener); camera.add(listener);
return () => { return () => {
camera.remove(listener); camera.remove(listener);
}; };
}, [camera]); }, [camera]);
return null; return null;
} }
function FearStateUpdater() { function FearStateUpdater() {
useFrame((state, delta) => { useFrame((state, delta) => {
fearState.update(delta); fearState.update(delta);
}); });
return null; return null;
} }
export default function Fear() { export default function Fear() {
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive); const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
const [wasCaught, setWasCaught] = useState(fearState.isRustActive); const [wasCaught, setWasCaught] = useState(fearState.isRustActive);
useEffect(() => { useEffect(() => {
const unsubscribe = fearState.subscribe(() => { const unsubscribe = fearState.subscribe(() => {
setIsRustActive(fearState.isRustActive); setIsRustActive(fearState.isRustActive);
setWasCaught(fearState.wasCaught) setWasCaught(fearState.wasCaught);
}); });
return () => unsubscribe(); return () => unsubscribe();
}, []); }, []);
return (<> return (
<Canvas <>
shadows <Canvas
gl={{ antialias: true }} shadows
className='canvas' gl={{ antialias: true }}
camera={{ position: [0, 3, -5], fov: 55, far: 100 }} className='canvas'
> camera={{ position: [0, 3, -5], fov: 55, far: 100 }}
<FearStateUpdater /> >
<FearStateUpdater />
<ListenerCreator /> <ListenerCreator />
<color attach="background" args={['#050505']} /> <color attach='background' args={['#050505']} />
{FEAR_SETTINGS.TEST_MODE ? <ambientLight intensity={2} /> : <ambientLight intensity={0.0225} />} {FEAR_SETTINGS.TEST_MODE ? (
{FEAR_SETTINGS.TEST_MODE ? null : <fogExp2 attach='fog' args={[0x050505, 0.035]} />} <ambientLight intensity={2} />
{FEAR_SETTINGS.TEST_MODE ? null : < PostProcessing />} ) : (
<ambientLight intensity={0.0225} />
)}
{FEAR_SETTINGS.TEST_MODE ? null : (
<fogExp2 attach='fog' args={[0x050505, 0.035]} />
)}
{FEAR_SETTINGS.TEST_MODE ? null : <PostProcessing />}
<Suspense fallback={null}> <Suspense fallback={null}>
<Hallway /> <Hallway />
<TheCreature /> <TheCreature />
<Player /> <Player />
</Suspense> </Suspense>
<AmbientSound <AmbientSound
key="ambient-1" key='ambient-1'
url='fear/snd/ambience.mp3' url='fear/snd/ambience.mp3'
volume={isRustActive ? 0 : 0.5} volume={isRustActive ? 0 : 0.5}
/> />
<AmbientSound <AmbientSound
key="ambient-2" key='ambient-2'
url='fear/snd/ambience2.mp3' url='fear/snd/ambience2.mp3'
volume={isRustActive ? 1 : 0} volume={isRustActive ? 1 : 0}
/> />
{wasCaught ? <AmbientSound {wasCaught ? (
key="ambient-glitch" <AmbientSound
url='fear/snd/glitch.mp3' key='ambient-glitch'
volume={1} url='fear/snd/glitch.mp3'
/> : null} volume={1}
</Canvas> />
) : null}
</Canvas>
<FinaleText /> <FinaleText />
</>) </>
} );
}
+51 -48
View File
@@ -1,64 +1,67 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react';
interface AmbientSoundProps { interface AmbientSoundProps {
url: string url: string;
volume?: number volume?: number;
} }
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) { export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
const audioRef = useRef<HTMLAudioElement | null>(null) const audioRef = useRef<HTMLAudioElement | null>(null);
const targetVolumeRef = useRef<number>(volume) const targetVolumeRef = useRef<number>(volume);
targetVolumeRef.current = volume targetVolumeRef.current = volume;
useEffect(() => { useEffect(() => {
const audio = new Audio(url) const audio = new Audio(url);
audio.loop = true audio.loop = true;
audio.volume = 0 audio.volume = 0;
audioRef.current = audio audioRef.current = audio;
let componentsMounted = true let componentsMounted = true;
const attemptPlay = () => { const attemptPlay = () => {
if (!audioRef.current || !componentsMounted) return if (!audioRef.current || !componentsMounted) return;
audio.volume = targetVolumeRef.current
if (audio.volume > 0 && audio.paused) { audio.volume = targetVolumeRef.current;
audio.play().catch((err) => {
console.warn('Autoplay management holding clip playback execution.', err)
})
}
}
attemptPlay() if (audio.volume > 0 && audio.paused) {
audio.play().catch((err) => {
console.warn(
'Autoplay management holding clip playback execution.',
err
);
});
}
};
window.addEventListener('click', attemptPlay) attemptPlay();
window.addEventListener('keydown', attemptPlay)
return () => { window.addEventListener('click', attemptPlay);
componentsMounted = false window.addEventListener('keydown', attemptPlay);
window.removeEventListener('click', attemptPlay)
window.removeEventListener('keydown', attemptPlay)
audio.pause()
audio.src = ''
audioRef.current = null
}
}, [url])
useEffect(() => { return () => {
const audio = audioRef.current componentsMounted = false;
if (!audio) return window.removeEventListener('click', attemptPlay);
window.removeEventListener('keydown', attemptPlay);
audio.pause();
audio.src = '';
audioRef.current = null;
};
}, [url]);
if (volume === 0) { useEffect(() => {
if (!audio.paused) audio.pause() const audio = audioRef.current;
} else { if (!audio) return;
audio.volume = volume
if (audio.paused) {
audio.play().catch(() => {})
}
}
}, [volume])
return null if (volume === 0) {
} if (!audio.paused) audio.pause();
} else {
audio.volume = volume;
if (audio.paused) {
audio.play().catch(() => {});
}
}
}, [volume]);
return null;
}
+158 -158
View File
@@ -1,196 +1,196 @@
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, useMemo, 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"; 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 baseTexture = useTexture('fear/img/creature.png'); const baseTexture = useTexture('fear/img/creature.png');
const texture = useMemo(() => { const texture = useMemo(() => {
const t = baseTexture.clone(); const t = baseTexture.clone();
t.needsUpdate = true; t.needsUpdate = true;
return t; return t;
}, [baseTexture]); }, [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();
const [hasTriggered, setHasTriggered] = useState(false); const [hasTriggered, setHasTriggered] = useState(false);
const [isSpawned, setIsSpawned] = useState(false); const [isSpawned, setIsSpawned] = useState(false);
const globalDistance = useRef<number>(32); const globalDistance = useRef<number>(32);
const [finaleTriggered, setFinaleTriggered] = useState(fearState.finaleTriggered); const [finaleTriggered, setFinaleTriggered] = useState(
fearState.finaleTriggered
);
const audioPlaying = useRef<boolean>(false); const audioPlaying = useRef<boolean>(false);
const movePhase = useRef<'frozen' | 'lurching'>('frozen'); const movePhase = useRef<'frozen' | 'lurching'>('frozen');
const phaseTimer = useRef<number>(1.5); const phaseTimer = useRef<number>(1.5);
const glitchCooldown = useRef<number>(0); const glitchCooldown = useRef<number>(0);
const isGlitchSpiking = useRef<boolean>(false); const isGlitchSpiking = useRef<boolean>(false);
const flickerCooldown = useRef<number>(0); const flickerCooldown = useRef<number>(0);
useEffect(() => { useEffect(() => {
const unsubscribe = fearState.subscribe(() => { const unsubscribe = fearState.subscribe(() => {
setFinaleTriggered(fearState.finaleTriggered); setFinaleTriggered(fearState.finaleTriggered);
if (!fearState.finaleTriggered) { if (!fearState.finaleTriggered) {
setIsSpawned(false); setIsSpawned(false);
setHasTriggered(false); setHasTriggered(false);
globalDistance.current = 32; globalDistance.current = 32;
audioPlaying.current = false; audioPlaying.current = false;
movePhase.current = 'frozen'; movePhase.current = 'frozen';
phaseTimer.current = 1.5; phaseTimer.current = 1.5;
if (audioRef.current && audioRef.current.isPlaying) if (audioRef.current && audioRef.current.isPlaying)
audioRef.current.stop(); audioRef.current.stop();
} }
}); });
return () => unsubscribe(); return () => unsubscribe();
}, []); }, []);
useFrame((state, delta) => { useFrame((state, delta) => {
if (!fearState.finaleTriggered) return; if (!fearState.finaleTriggered) return;
const creature = meshRef.current; const creature = meshRef.current;
if (!creature) return; if (!creature) return;
if (!isSpawned) { if (!isSpawned) {
setIsSpawned(true); setIsSpawned(true);
globalDistance.current = 32; globalDistance.current = 32;
movePhase.current = 'frozen'; movePhase.current = 'frozen';
phaseTimer.current = 1.0 + Math.random() * 1.5; 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) {
phaseTimer.current -= delta;
if (hasTriggered) { if (phaseTimer.current <= 0) {
phaseTimer.current -= delta; 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 === 'lurching') {
if (movePhase.current === 'frozen') { globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * 3 * delta;
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') { if (audioRef.current && !audioPlaying.current) {
globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * 3 * delta; audioPlaying.current = true;
} if (audioRef.current.context.state === 'suspended')
audioRef.current.context.resume();
audioRef.current.play();
}
if (audioRef.current && !audioPlaying.current) { const shakeIntensity =
audioPlaying.current = true; Math.max(0, 1 - globalDistance.current / 32) * 0.22;
if (audioRef.current.context.state === 'suspended') camera.position.x += (Math.random() - 0.5) * shakeIntensity;
audioRef.current.context.resume(); camera.position.y += (Math.random() - 0.5) * shakeIntensity;
audioRef.current.play();
}
const shakeIntensity = Math.max(0, 1 - (globalDistance.current / 32)) * 0.22; if (globalDistance.current <= 0.1) {
camera.position.x += (Math.random() - 0.5) * shakeIntensity; window.location.href = '/';
camera.position.y += (Math.random() - 0.5) * shakeIntensity; fearState.registerCaught();
return;
}
}
if (globalDistance.current <= 0.1) { const forwardVector = new THREE.Vector3();
window.location.href = '/'; camera.getWorldDirection(forwardVector);
fearState.registerCaught(); const lookDirZ = forwardVector.z < 0 ? -1 : 1;
return;
}
}
const forwardVector = new THREE.Vector3(); const calculatedZ = camera.position.z + lookDirZ * globalDistance.current;
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);
creature.position.set(0, 1.6, calculatedZ); if (!hasTriggered) return;
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;
const proximity = 1 - Math.max(0, Math.min(1, globalDistance.current / 32)); glitchCooldown.current -= delta;
const jitterX = 1.0 + (Math.random() - 0.5) * 0.04 * proximity; if (glitchCooldown.current <= 0) {
let jitterY = 1.0 + (Math.random() - 0.5) * 0.06 * proximity; 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 (isGlitchSpiking.current) {
if (glitchCooldown.current <= 0) { const spike = 0.15 + Math.random() * 0.35;
if (Math.random() < 0.25 + proximity * 0.35) { jitterY += Math.random() > 0.5 ? spike : -spike * 0.6;
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) { creature.scale.set(jitterX, jitterY, 1.0);
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);
}
}
flickerCooldown.current -= delta; texture.offset.set(
if (flickerCooldown.current <= 0) { (Math.random() - 0.5) * 0.025 * proximity,
if (creature.visible && Math.random() < 0.12 + proximity * 0.08) { (Math.random() - 0.5) * 0.025 * proximity
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( if (proximity > 0.2) {
(Math.random() - 0.5) * 0.025 * proximity, creature.position.x += (Math.random() - 0.5) * 0.12 * proximity;
(Math.random() - 0.5) * 0.025 * proximity creature.position.y += (Math.random() - 0.5) * 0.06 * proximity;
); }
});
if (proximity > 0.2) { return (
creature.position.x += (Math.random() - 0.5) * 0.12 * proximity; <mesh ref={meshRef} visible={finaleTriggered}>
creature.position.y += (Math.random() - 0.5) * 0.06 * proximity; <planeGeometry args={[3.0, 4.8]} />
} <meshStandardMaterial
}); map={texture}
transparent={true}
depthWrite={false}
side={THREE.DoubleSide}
onBeforeCompile={ShaderPatch}
emissive='#ffffff'
emissiveMap={texture}
emissiveIntensity={0.15}
/>
return ( {finaleTriggered && (
<mesh <PositionalAudio
ref={meshRef} url='fear/snd/riser.mp3'
visible={finaleTriggered} ref={audioRef}
> distance={25}
<planeGeometry args={[3.0, 4.8]} /> loop={false}
<meshStandardMaterial autoplay={false}
map={texture} />
transparent={true} )}
depthWrite={false} </mesh>
side={THREE.DoubleSide} );
onBeforeCompile={ShaderPatch} }
emissive="#ffffff"
emissiveMap={texture}
emissiveIntensity={0.15}
/>
{finaleTriggered && (
<PositionalAudio
url="fear/snd/riser.mp3"
ref={audioRef}
distance={25}
loop={false}
autoplay={false}
/>
)}
</mesh>
);
}
+61 -63
View File
@@ -1,84 +1,82 @@
@font-face { @font-face {
font-family: 'VCR'; font-family: 'VCR';
src: url('/fear/fonts/vcr.ttf') format('truetype'); src: url('/fear/fonts/vcr.ttf') format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
.finale-container { .finale-container {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
left: 0; left: 0;
top: 0vh; top: 0vh;
display: grid; display: grid;
align-items: center; align-items: center;
align-content: center; align-content: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
/* filter: invert(100%); */ /* filter: invert(100%); */
backdrop-filter: brightness(100%); backdrop-filter: brightness(100%);
grid-auto-rows: 5vh; grid-auto-rows: 5vh;
/* grid-template-columns: 0; */ /* grid-template-columns: 0; */
grid-template-rows: repeat(auto-fit, max-content); grid-template-rows: repeat(auto-fit, max-content);
user-select: none; user-select: none;
will-change: filter;
animation: invertFlicker 0.07s infinite alternate;
will-change: filter;
animation: invertFlicker 0.07s infinite alternate;
} }
@keyframes invertFlicker { @keyframes invertFlicker {
0%,
43%,
45%,
88%,
92% {
filter: invert(0%) contrast(100%) brightness(100%);
backdrop-filter: brightness(100%) hue-rotate(0deg);
}
0%, 44%,
43%, 46%,
45%, 89%,
88%, 93%,
92% { 100% {
filter: invert(0%) contrast(100%) brightness(100%); filter: invert(100%) contrast(300%) brightness(150%);
backdrop-filter: brightness(100%) hue-rotate(0deg); 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 { .finale-text {
font-family: 'VCR', sans-serif; font-family: 'VCR', sans-serif;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
letter-spacing: 0.1em; letter-spacing: 0.1em;
height: 0px; height: 0px;
width: 100%; width: 100%;
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
font-size: 8vh; font-size: 8vh;
text-align: center;
white-space: nowrap;
text-align: center;
white-space: nowrap;
} }
.scanlines { .scanlines {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 900; z-index: 900;
background: repeating-linear-gradient(rgba(0, 0, 0, 0) 0px, background: repeating-linear-gradient(
rgba(0, 0, 0, 0) 2px, rgba(0, 0, 0, 0) 0px,
rgba(0, 0, 0, 0.3) 2px, rgba(0, 0, 0, 0) 2px,
rgba(0, 0, 0, 0.3) 4px); rgba(0, 0, 0, 0.3) 2px,
rgba(0, 0, 0, 0.3) 4px
} );
}
+71 -41
View File
@@ -1,56 +1,86 @@
import { JSX, useEffect, useState } from "react" import { JSX, useEffect, useState } from 'react';
import { fearState } from "../state" import { fearState } from '../state';
import './finale-text.css'; import './finale-text.css';
const BLOCKS = [ const BLOCKS = [
"▀", "▂", "▃", "▄", "▅", "▆", "▇", '▀',
"█", "▉", "▊", "▋", "▌", "▍", "▎", "▏", '▂',
"▐", "░", "▒", "▓", "▔", "▕", "▖", "▗", '▃',
"▘", "▙", "▚", "▛", "▜", "▝", "▞", "▟" '▄',
'▅',
'▆',
'▇',
'█',
'▉',
'▊',
'▋',
'▌',
'▍',
'▎',
'▏',
'▐',
'░',
'▒',
'▓',
'▔',
'▕',
'▖',
'▗',
'▘',
'▙',
'▚',
'▛',
'▜',
'▝',
'▞',
'▟'
]; ];
export default function FinaleText() { export default function FinaleText() {
const [wasCaught, setWasCaught] = useState(fearState.wasCaught); const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
const [elements, setElements] = useState<JSX.Element[]>([]); const [elements, setElements] = useState<JSX.Element[]>([]);
useEffect(() => { useEffect(() => {
const unsubscribe = fearState.subscribe(() => { const unsubscribe = fearState.subscribe(() => {
setWasCaught(fearState.wasCaught) setWasCaught(fearState.wasCaught);
}); });
return () => unsubscribe(); return () => unsubscribe();
}, []); }, []);
useEffect(() => {
if (!wasCaught) return;
useEffect(() => { const interval = setInterval(() => {
if (!wasCaught) if (Math.random() > 0.9) return;
return;
const interval = setInterval(() => { const baseText = 'bwaaaaaaaaa';
if (Math.random() > 0.9) return; const corrupted = baseText
.split('')
.map((char) =>
Math.random() > 0.98
? BLOCKS[Math.floor(Math.random() * BLOCKS.length)]
: char
)
.join('');
const baseText = "bwaaaaaaaaa"; setElements((prev) => [
const corrupted = baseText ...prev.slice(-30),
.split("") <span className='finale-text' key={crypto.randomUUID()}>
.map((char) => (Math.random() > 0.98 ? BLOCKS[Math.floor(Math.random() * BLOCKS.length)] : char)) {corrupted}
.join(""); </span>
]);
}, 10);
setElements((prev) => [...prev.slice(-30), return () => clearInterval(interval);
<span className="finale-text" key={crypto.randomUUID()}> }, [wasCaught]);
{corrupted}
</span>
]);
}, 10);
return () => clearInterval(interval); if (!wasCaught) return null;
}, [wasCaught]);
if (!wasCaught) return null; return (
<>
return (<> <div className='finale-container'>{elements}</div>
<div className="finale-container"> <div className='scanlines' />
{elements} </>
</div> );
<div className="scanlines" /> }
</>)
}
+472 -341
View File
@@ -1,401 +1,532 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from 'react';
import { FEAR_SETTINGS, fearState } from "../state"; import { FEAR_SETTINGS, fearState } from '../state';
import { useTexture, PositionalAudio } from "@react-three/drei"; import { useTexture, PositionalAudio } from '@react-three/drei';
import * as THREE from "three"; import * as THREE from 'three';
import { useFrame } from "@react-three/fiber"; import { useFrame } from '@react-three/fiber';
import { ShaderPatch } from "../shader-patch"; import { ShaderPatch } from '../shader-patch';
interface DoorProps { interface DoorProps {
position: [number, number, number]; position: [number, number, number];
rotation: [number, number, number]; rotation: [number, number, number];
} }
function Door({ position, rotation }: DoorProps) { function Door({ position, rotation }: DoorProps) {
const [soundUrl, setSoundUrl] = useState<string | null>(null); const [soundUrl, setSoundUrl] = useState<string | null>(null);
const currentSound = useRef<string | null>(null); const currentSound = useRef<string | null>(null);
const steelTex = useTexture('fear/img/steel.png'); const steelTex = useTexture('fear/img/steel.png');
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
if (Math.random() < 0.02) { if (Math.random() < 0.02) {
const chosenSound = Math.random() < 0.5 ? "fear/snd/knock1.mp3" : "fear/snd/knock2.mp3"; const chosenSound =
Math.random() < 0.5 ? 'fear/snd/knock1.mp3' : 'fear/snd/knock2.mp3';
setSoundUrl(chosenSound); setSoundUrl(chosenSound);
currentSound.current = chosenSound; currentSound.current = chosenSound;
} }
}, 5000); }, 5000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
const handleAudioEnded = () => { const handleAudioEnded = () => {
setSoundUrl(null); setSoundUrl(null);
currentSound.current = null; currentSound.current = null;
}; };
return ( return (
<group position={position} rotation={rotation}> <group position={position} rotation={rotation}>
{/* frame */} {/* frame */}
<mesh position={[0, 2, -0.1]}> <mesh position={[0, 2, -0.1]}>
<boxGeometry args={[2.4, 4.0, 0.2, 4, 4, 1]} /> <boxGeometry args={[2.4, 4.0, 0.2, 4, 4, 1]} />
<meshStandardMaterial map={steelTex} color="#8d8d8d" onBeforeCompile={ShaderPatch} /> <meshStandardMaterial
</mesh> map={steelTex}
color='#8d8d8d'
onBeforeCompile={ShaderPatch}
/>
</mesh>
{/* panel */} {/* panel */}
<mesh position={[0, 1.95, -0.0]}> <mesh position={[0, 1.95, -0.0]}>
<boxGeometry args={[2.1, 3.8, 0.1, 4, 4, 1]} /> <boxGeometry args={[2.1, 3.8, 0.1, 4, 4, 1]} />
<meshStandardMaterial map={steelTex} color="#4e4a4a" onBeforeCompile={ShaderPatch} /> <meshStandardMaterial
</mesh> map={steelTex}
color='#4e4a4a'
onBeforeCompile={ShaderPatch}
/>
</mesh>
{/* handle */} {/* handle */}
<mesh position={[0.75, 1.8, .085]}> <mesh position={[0.75, 1.8, 0.085]}>
<boxGeometry args={[0.3, 0.08, 0.1]} /> <boxGeometry args={[0.3, 0.08, 0.1]} />
<meshStandardMaterial map={steelTex} color="#ffffff" onBeforeCompile={ShaderPatch} /> <meshStandardMaterial
</mesh> map={steelTex}
color='#ffffff'
onBeforeCompile={ShaderPatch}
/>
</mesh>
{soundUrl && ( {soundUrl && (
<PositionalAudio <PositionalAudio
url={soundUrl} url={soundUrl}
distance={25} distance={25}
loop={false} loop={false}
autoplay={true} autoplay={true}
onEnded={handleAudioEnded} onEnded={handleAudioEnded}
/> />
)} )}
</group> </group>
); );
} }
export default function Hallway() { export default function Hallway() {
const [width, setWidth] = useState(fearState.currentWidth); const [width, setWidth] = useState(fearState.currentWidth);
const [floorTex, wallTex, rustWallTex, rustFloorTex] = useTexture([ const [floorTex, wallTex, rustWallTex, rustFloorTex] = useTexture([
'fear/img/concrete-floor.png', 'fear/img/concrete-floor.png',
'fear/img/concrete-wall.png', 'fear/img/concrete-wall.png',
'fear/img/rust.png', 'fear/img/rust.png',
'fear/img/rust.png' 'fear/img/rust.png'
]); ]);
useEffect(() => { useEffect(() => {
[floorTex, wallTex, rustWallTex, rustFloorTex].forEach((tex) => { [floorTex, wallTex, rustWallTex, rustFloorTex].forEach((tex) => {
tex.wrapS = tex.wrapT = THREE.RepeatWrapping; tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.minFilter = tex.magFilter = THREE.NearestFilter; tex.minFilter = tex.magFilter = THREE.NearestFilter;
tex.colorSpace = THREE.SRGBColorSpace; tex.colorSpace = THREE.SRGBColorSpace;
}); });
}, [floorTex, wallTex, rustWallTex, rustFloorTex]); }, [floorTex, wallTex, rustWallTex, rustFloorTex]);
const segmentPool = [0, 1, 2, 3, 4];
const segmentCount = segmentPool.length;
const segmentPool = [0, 1, 2, 3, 4]; const lightRefs = useRef<(THREE.PointLight | null)[]>([]);
const segmentCount = segmentPool.length; const matRefs = useRef<(THREE.MeshStandardMaterial | null)[]>([]);
const lightRefs = useRef<(THREE.PointLight | null)[]>([]); const lightState = useRef<'normal' | 'flickering' | 'dead'>('normal');
const matRefs = useRef<(THREE.MeshStandardMaterial | null)[]>([]); const stateEndTime = useRef<number>(0);
const nextEventTime = useRef<number>(5);
const lightState = useRef<'normal' | 'flickering' | 'dead'>('normal'); const segmentsRef = useRef<THREE.Group[]>([]);
const stateEndTime = useRef<number>(0); const wallMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
const nextEventTime = useRef<number>(5); const floorMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
const pipeMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
const bracketMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
const segmentsRef = useRef<THREE.Group[]>([]); wallMaterialsRef.current = [];
const wallMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]); floorMaterialsRef.current = [];
const floorMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]); pipeMaterialsRef.current = [];
const pipeMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]); bracketMaterialsRef.current = [];
const bracketMaterialsRef = useRef<THREE.MeshStandardMaterial[]>([]);
wallMaterialsRef.current = []; const [isRustActive, setIsRustActive] = useState(fearState.isRustActive);
floorMaterialsRef.current = [];
pipeMaterialsRef.current = [];
bracketMaterialsRef.current = [];
const [isRustActive, setIsRustActive] = useState(fearState.isRustActive); useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWidth(fearState.currentWidth);
setIsRustActive(fearState.isRustActive);
});
return () => unsubscribe();
}, []);
useEffect(() => { useFrame((state, delta) => {
const unsubscribe = fearState.subscribe(() => { const time = state.clock.elapsedTime;
setWidth(fearState.currentWidth);
setIsRustActive(fearState.isRustActive);
});
return () => unsubscribe();
}, []);
useFrame((state, delta) => { /*
const time = state.clock.elapsedTime;
/*
lights lights
*/ */
let intensity1 = 0.85 + Math.sin(time * 2) * 0.03; let intensity1 = 0.85 + Math.sin(time * 2) * 0.03;
if (time > nextEventTime.current && lightState.current === 'normal') { if (time > nextEventTime.current && lightState.current === 'normal') {
lightState.current = 'flickering'; lightState.current = 'flickering';
stateEndTime.current = time + 1.5 + Math.random() * 2; stateEndTime.current = time + 1.5 + Math.random() * 2;
} }
if (lightState.current === 'flickering') { if (lightState.current === 'flickering') {
if (time > stateEndTime.current) { if (time > stateEndTime.current) {
if (Math.random() > 0.4) { if (Math.random() > 0.4) {
lightState.current = 'dead'; lightState.current = 'dead';
stateEndTime.current = time + 1.0 + Math.random() * 2.5; stateEndTime.current = time + 1.0 + Math.random() * 2.5;
} else { } else {
lightState.current = 'normal'; lightState.current = 'normal';
nextEventTime.current = time + 10 + Math.random() * 20; nextEventTime.current = time + 10 + Math.random() * 20;
} }
} else { } else {
const baseWave = Math.sin(time * 45) * 0.4 + Math.sin(time * 90) * 0.3; const baseWave = Math.sin(time * 45) * 0.4 + Math.sin(time * 90) * 0.3;
intensity1 = 0.5 + baseWave; 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 (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) { if (lightState.current === 'dead') {
lightState.current = 'normal'; if (time > stateEndTime.current) {
nextEventTime.current = time + 12 + Math.random() * 15; lightState.current = 'normal';
} else { nextEventTime.current = time + 12 + Math.random() * 15;
intensity1 = Math.random() > 0.98 ? 0.08 : 0.0; } else {
} intensity1 = Math.random() > 0.98 ? 0.08 : 0.0;
} }
}
/* /*
objects objects
*/ */
const length = FEAR_SETTINGS.HALLWAY_LENGTH; const length = FEAR_SETTINGS.HALLWAY_LENGTH;
const playerSegmentZ = Math.floor(state.camera.position.z / length); const playerSegmentZ = Math.floor(state.camera.position.z / length);
const horizontalTexRepeat = width / FEAR_SETTINGS.HALLWAY_WIDTH; const horizontalTexRepeat = width / FEAR_SETTINGS.HALLWAY_WIDTH;
floorTex.repeat.set(horizontalTexRepeat, 10); floorTex.repeat.set(horizontalTexRepeat, 10);
wallTex.repeat.set(10, 1); wallTex.repeat.set(10, 1);
rustWallTex.repeat.set(10, 1); rustWallTex.repeat.set(10, 1);
rustFloorTex.repeat.set(horizontalTexRepeat, 10); rustFloorTex.repeat.set(horizontalTexRepeat, 10);
floorTex.needsUpdate = true; floorTex.needsUpdate = true;
wallTex.needsUpdate = true; wallTex.needsUpdate = true;
rustWallTex.needsUpdate = true; rustWallTex.needsUpdate = true;
rustFloorTex.needsUpdate = true; rustFloorTex.needsUpdate = true;
let closestPoolIndex = 0; let closestPoolIndex = 0;
let minDistance = Infinity; let minDistance = Infinity;
segmentsRef.current.forEach((segGroup, poolIndex) => { segmentsRef.current.forEach((segGroup, poolIndex) => {
if (!segGroup) return; if (!segGroup) return;
let segmentZIndex = poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ; let segmentZIndex =
segGroup.position.z = segmentZIndex * length; poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ;
segGroup.position.z = segmentZIndex * length;
const distance = Math.abs(segGroup.position.z - state.camera.position.z); const distance = Math.abs(segGroup.position.z - state.camera.position.z);
if (distance < minDistance) { if (distance < minDistance) {
minDistance = distance; minDistance = distance;
closestPoolIndex = poolIndex; closestPoolIndex = poolIndex;
} }
const leftWallGroup = segGroup.getObjectByName("left-wall-group"); const leftWallGroup = segGroup.getObjectByName('left-wall-group');
if (leftWallGroup) leftWallGroup.position.x = -width / 2; if (leftWallGroup) leftWallGroup.position.x = -width / 2;
const rightWallGroup = segGroup.getObjectByName("right-wall-group"); const rightWallGroup = segGroup.getObjectByName('right-wall-group');
if (rightWallGroup) rightWallGroup.position.x = width / 2; if (rightWallGroup) rightWallGroup.position.x = width / 2;
const floorMesh = segGroup.getObjectByName("floor-mesh"); const floorMesh = segGroup.getObjectByName('floor-mesh');
if (floorMesh) floorMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH; if (floorMesh) floorMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH;
const ceilingMesh = segGroup.getObjectByName("ceiling-mesh"); const ceilingMesh = segGroup.getObjectByName('ceiling-mesh');
if (ceilingMesh) ceilingMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH; if (ceilingMesh)
ceilingMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH;
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const pipe = segGroup.getObjectByName(`pipe-${i}`); const pipe = segGroup.getObjectByName(`pipe-${i}`);
if (pipe) pipe.position.x = -width / 2 + 0.4 + (i * 0.20); if (pipe) pipe.position.x = -width / 2 + 0.4 + i * 0.2;
} }
const bracketGroup = segGroup.getObjectByName("brackets-group"); const bracketGroup = segGroup.getObjectByName('brackets-group');
if (bracketGroup) { if (bracketGroup) {
bracketGroup.children.forEach(b => { bracketGroup.children.forEach((b) => {
b.position.x = -width / 2 + 0.6; b.position.x = -width / 2 + 0.6;
}); });
} }
}); });
/* /*
dyn light dyn light
*/ */
segmentPool.forEach((poolIndex) => { segmentPool.forEach((poolIndex) => {
const light = lightRefs.current[poolIndex]; const light = lightRefs.current[poolIndex];
const mat = matRefs.current[poolIndex]; const mat = matRefs.current[poolIndex];
if (poolIndex === closestPoolIndex) { if (poolIndex === closestPoolIndex) {
if (light) light.intensity = intensity1 * 1.2; if (light) light.intensity = intensity1 * 1.2;
if (mat) { if (mat) {
mat.emissiveIntensity = intensity1 * 2.5; mat.emissiveIntensity = intensity1 * 2.5;
if (lightState.current !== 'normal') mat.emissive.setHSL(0.07, 0.4, Math.min(intensity1, 0.7)); if (lightState.current !== 'normal')
else mat.emissive.setHex(0xa8a1a1); mat.emissive.setHSL(0.07, 0.4, Math.min(intensity1, 0.7));
} else mat.emissive.setHex(0xa8a1a1);
} else { }
if (light) light.intensity = 0.9; } else {
if (mat) { if (light) light.intensity = 0.9;
mat.emissiveIntensity = 0.8; if (mat) {
mat.emissive.setHex(0xa8a1a1); mat.emissiveIntensity = 0.8;
} mat.emissive.setHex(0xa8a1a1);
} }
}); }
});
/* /*
materials materials
*/ */
const updateMaterials = (materials: THREE.MeshStandardMaterial[], defaultTex: THREE.Texture, targetRustTex: THREE.Texture, activeColor: string, defaultColor: string, activeRough: number, defaultRough: number, activeMetal: number, defaultMetal: number) => { const updateMaterials = (
materials.forEach(mat => { materials: THREE.MeshStandardMaterial[],
if (!mat) return; defaultTex: THREE.Texture,
const targetTex = isRustActive ? targetRustTex : defaultTex; targetRustTex: THREE.Texture,
if (mat.map !== targetTex) { activeColor: string,
mat.map = targetTex; defaultColor: string,
mat.needsUpdate = true; activeRough: number,
} defaultRough: number,
mat.color.set(isRustActive ? activeColor : defaultColor); activeMetal: number,
mat.roughness = isRustActive ? activeRough : defaultRough; defaultMetal: number
mat.metalness = isRustActive ? activeMetal : defaultMetal; ) => {
}); 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(
updateMaterials(floorMaterialsRef.current, floorTex, rustFloorTex, "#cabdb9", "#ffffff", 0.95, 0.8, 0.05, 0.2); 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 => { pipeMaterialsRef.current.forEach((mat) => {
if (!mat) return; if (!mat) return;
mat.color.set(isRustActive ? "#3d1b0f" : "#a5aca8"); mat.color.set(isRustActive ? '#3d1b0f' : '#a5aca8');
mat.roughness = isRustActive ? 0.95 : 0.0; mat.roughness = isRustActive ? 0.95 : 0.0;
mat.metalness = isRustActive ? 0.05 : 0.4; mat.metalness = isRustActive ? 0.05 : 0.4;
}); });
bracketMaterialsRef.current.forEach(mat => { bracketMaterialsRef.current.forEach((mat) => {
if (!mat) return; if (!mat) return;
mat.color.set(isRustActive ? "#1b0b05" : "#a5aca8"); mat.color.set(isRustActive ? '#1b0b05' : '#a5aca8');
mat.roughness = isRustActive ? 0.95 : 0.0; mat.roughness = isRustActive ? 0.95 : 0.0;
mat.metalness = isRustActive ? 0.05 : 0.4; mat.metalness = isRustActive ? 0.05 : 0.4;
}); });
}); });
return ( return (
<> <>
{segmentPool.map((poolIndex) => ( {segmentPool.map((poolIndex) => (
<group <group
key={poolIndex} key={poolIndex}
ref={(el) => { if (el) segmentsRef.current[poolIndex] = el; }} ref={(el) => {
position={[0, 0, 0]} if (el) segmentsRef.current[poolIndex] = el;
> }}
{/* lights */} position={[0, 0, 0]}
<group position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT - 0.1, -FEAR_SETTINGS.HALLWAY_LENGTH / 4]}> >
<pointLight {/* lights */}
ref={(el) => { lightRefs.current[poolIndex] = el; }} <group
intensity={0.9} position={[
distance={15} 0,
color="#a8a1a1" FEAR_SETTINGS.HALLWAY_HEIGHT - 0.1,
/> -FEAR_SETTINGS.HALLWAY_LENGTH / 4
<mesh position={[0, 0.09, 0]}> ]}
<boxGeometry args={[0.3, 0.01, 0.3]} /> >
<meshStandardMaterial <pointLight
ref={(el) => { matRefs.current[poolIndex] = el; }} ref={(el) => {
color="#111111" lightRefs.current[poolIndex] = el;
emissive="#a8a1a1" }}
emissiveIntensity={0.8} intensity={0.9}
roughness={0.9} distance={15}
onBeforeCompile={ShaderPatch} color='#a8a1a1'
/> />
</mesh> <mesh position={[0, 0.09, 0]}>
</group> <boxGeometry args={[0.3, 0.01, 0.3]} />
<meshStandardMaterial
ref={(el) => {
matRefs.current[poolIndex] = el;
}}
color='#111111'
emissive='#a8a1a1'
emissiveIntensity={0.8}
roughness={0.9}
onBeforeCompile={ShaderPatch}
/>
</mesh>
</group>
{/* floor */} {/* floor */}
<mesh <mesh
name="floor-mesh" name='floor-mesh'
rotation={[-Math.PI / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}
position={[0, 0, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]} position={[0, 0, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
> >
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH, 4, 10]} /> <planeGeometry
<meshStandardMaterial args={[
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }} FEAR_SETTINGS.HALLWAY_WIDTH,
map={floorTex} FEAR_SETTINGS.HALLWAY_LENGTH,
onBeforeCompile={ShaderPatch} 4,
/> 10
</mesh> ]}
/>
<meshStandardMaterial
ref={(el) => {
if (el) floorMaterialsRef.current.push(el);
}}
map={floorTex}
onBeforeCompile={ShaderPatch}
/>
</mesh>
{/* ceiling */} {/* ceiling */}
<mesh <mesh
name="ceiling-mesh" name='ceiling-mesh'
rotation={[Math.PI / 2, 0, 0]} rotation={[Math.PI / 2, 0, 0]}
position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]} position={[
> 0,
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH, 4, 10]} /> FEAR_SETTINGS.HALLWAY_HEIGHT,
<meshStandardMaterial -FEAR_SETTINGS.HALLWAY_LENGTH / 2
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }} ]}
map={floorTex} >
onBeforeCompile={ShaderPatch} <planeGeometry
/> args={[
</mesh> FEAR_SETTINGS.HALLWAY_WIDTH,
FEAR_SETTINGS.HALLWAY_LENGTH,
4,
10
]}
/>
<meshStandardMaterial
ref={(el) => {
if (el) floorMaterialsRef.current.push(el);
}}
map={floorTex}
onBeforeCompile={ShaderPatch}
/>
</mesh>
{/* left wall */} {/* left wall */}
<group name="left-wall-group"> <group name='left-wall-group'>
<mesh rotation={[0, Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}> <mesh
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT, 10, 4]} /> rotation={[0, Math.PI / 2, 0]}
<meshStandardMaterial position={[
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }} 0,
map={wallTex} FEAR_SETTINGS.HALLWAY_HEIGHT / 2,
onBeforeCompile={ShaderPatch} -FEAR_SETTINGS.HALLWAY_LENGTH / 2
/> ]}
</mesh> >
{!isRustActive && ( <planeGeometry
<> args={[
<Door position={[0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.25]} rotation={[0, Math.PI / 2, 0]} /> FEAR_SETTINGS.HALLWAY_LENGTH,
<Door position={[0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.85]} rotation={[0, Math.PI / 2, 0]} /> FEAR_SETTINGS.HALLWAY_HEIGHT,
</> 10,
)} 4
</group> ]}
/>
<meshStandardMaterial
ref={(el) => {
if (el) wallMaterialsRef.current.push(el);
}}
map={wallTex}
onBeforeCompile={ShaderPatch}
/>
</mesh>
{!isRustActive && (
<>
<Door
position={[0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.25]}
rotation={[0, Math.PI / 2, 0]}
/>
<Door
position={[0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.85]}
rotation={[0, Math.PI / 2, 0]}
/>
</>
)}
</group>
{/* right wall */} {/* right wall */}
<group name="right-wall-group"> <group name='right-wall-group'>
<mesh rotation={[0, -Math.PI / 2, 0]} position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT / 2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}> <mesh
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_LENGTH, FEAR_SETTINGS.HALLWAY_HEIGHT, 10, 4]} /> rotation={[0, -Math.PI / 2, 0]}
<meshStandardMaterial position={[
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }} 0,
map={wallTex} FEAR_SETTINGS.HALLWAY_HEIGHT / 2,
onBeforeCompile={ShaderPatch} -FEAR_SETTINGS.HALLWAY_LENGTH / 2
/> ]}
</mesh> >
{!isRustActive && ( <planeGeometry
<Door position={[-0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.65]} rotation={[0, -Math.PI / 2, 0]} /> args={[
)} FEAR_SETTINGS.HALLWAY_LENGTH,
</group> FEAR_SETTINGS.HALLWAY_HEIGHT,
10,
4
]}
/>
<meshStandardMaterial
ref={(el) => {
if (el) wallMaterialsRef.current.push(el);
}}
map={wallTex}
onBeforeCompile={ShaderPatch}
/>
</mesh>
{!isRustActive && (
<Door
position={[-0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.65]}
rotation={[0, -Math.PI / 2, 0]}
/>
)}
</group>
{/* pipes */} {/* pipes */}
{Array.from({ length: 3 }).map((_, idx) => ( {Array.from({ length: 3 }).map((_, idx) => (
<mesh <mesh
key={idx} key={idx}
name={`pipe-${idx}`} name={`pipe-${idx}`}
rotation={[Math.PI / 2, 0, 0]} rotation={[Math.PI / 2, 0, 0]}
position={[-FEAR_SETTINGS.HALLWAY_WIDTH / 2 + 0.4 + (idx * 0.20), FEAR_SETTINGS.HALLWAY_HEIGHT - 0.2, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]} position={[
> -FEAR_SETTINGS.HALLWAY_WIDTH / 2 + 0.4 + idx * 0.2,
<cylinderGeometry args={[0.06, 0.06, FEAR_SETTINGS.HALLWAY_LENGTH, 4]} /> FEAR_SETTINGS.HALLWAY_HEIGHT - 0.2,
<meshStandardMaterial -FEAR_SETTINGS.HALLWAY_LENGTH / 2
ref={(el) => el && pipeMaterialsRef.current.push(el)} ]}
color="#a5aca8" >
roughness={0.0} <cylinderGeometry
metalness={0.4} args={[0.06, 0.06, FEAR_SETTINGS.HALLWAY_LENGTH, 4]}
onBeforeCompile={ShaderPatch} />
/> <meshStandardMaterial
</mesh> ref={(el) => el && pipeMaterialsRef.current.push(el)}
))} color='#a5aca8'
roughness={0.0}
metalness={0.4}
onBeforeCompile={ShaderPatch}
/>
</mesh>
))}
{/* brackets */} {/* brackets */}
<group name="brackets-group"> <group name='brackets-group'>
{Array.from({ length: 5 }).map((_, idx) => { {Array.from({ length: 5 }).map((_, idx) => {
const zOffset = -(idx * 8 + 4); const zOffset = -(idx * 8 + 4);
return ( return (
<mesh <mesh
key={`bracket-${idx}`} key={`bracket-${idx}`}
position={[-FEAR_SETTINGS.HALLWAY_WIDTH / 2 + 0.6, FEAR_SETTINGS.HALLWAY_HEIGHT - 0.15, zOffset]} position={[
> -FEAR_SETTINGS.HALLWAY_WIDTH / 2 + 0.6,
<boxGeometry args={[0.7, 0.3, 0.15]} /> FEAR_SETTINGS.HALLWAY_HEIGHT - 0.15,
<meshStandardMaterial zOffset
ref={(el) => el && bracketMaterialsRef.current.push(el)} ]}
color="#a5aca8" >
roughness={0.0} <boxGeometry args={[0.7, 0.3, 0.15]} />
metalness={0.4} <meshStandardMaterial
onBeforeCompile={ShaderPatch} ref={(el) => el && bracketMaterialsRef.current.push(el)}
/> color='#a5aca8'
</mesh> roughness={0.0}
); metalness={0.4}
})} onBeforeCompile={ShaderPatch}
</group> />
</group> </mesh>
))} );
</> })}
); </group>
</group>
))}
</>
);
} }
+173 -145
View File
@@ -1,8 +1,8 @@
import { useFrame, useThree } from "@react-three/fiber"; import { useFrame, useThree } from '@react-three/fiber';
import { useEffect, useRef } from "react"; import { useEffect, useRef } from 'react';
import { FEAR_SETTINGS, fearState } from "../state"; import { FEAR_SETTINGS, fearState } from '../state';
import { PointerLockControls } from "@react-three/drei"; import { PointerLockControls } from '@react-three/drei';
import * as THREE from "three"; import * as THREE from 'three';
const forward = new THREE.Vector3(); const forward = new THREE.Vector3();
const side = new THREE.Vector3(); const side = new THREE.Vector3();
@@ -14,181 +14,209 @@ const targetVelocity = new THREE.Vector3();
const currentVelocity = new THREE.Vector3(); const currentVelocity = new THREE.Vector3();
function usePlayerControls() { function usePlayerControls() {
const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false }); const keys = useRef({
Forward: false,
Backward: false,
Left: false,
Right: false
});
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = true; if (e.code === 'KeyW' || e.code === 'ArrowUp')
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = true; keys.current.Forward = true;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = true; if (e.code === 'KeyS' || e.code === 'ArrowDown')
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = true; 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) => { const handleKeyUp = (e: KeyboardEvent) => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = false; if (e.code === 'KeyW' || e.code === 'ArrowUp')
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = false; keys.current.Forward = false;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = false; if (e.code === 'KeyS' || e.code === 'ArrowDown')
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = false; 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('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp); window.addEventListener('keyup', handleKeyUp);
return () => { return () => {
window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp); window.removeEventListener('keyup', handleKeyUp);
}; };
}, []); }, []);
return keys.current; return keys.current;
} }
export default function Player() { export default function Player() {
const { camera } = useThree(); const { camera } = useThree();
const controls = usePlayerControls(); const controls = usePlayerControls();
const flashlightRef = useRef<THREE.SpotLight>(null); const flashlightRef = useRef<THREE.SpotLight>(null);
const movementCounter = useRef<number>(0); const movementCounter = useRef<number>(0);
const bobIntensity = useRef<number>(0); const bobIntensity = useRef<number>(0);
const confirmedSegment = useRef<number>(0); const confirmedSegment = useRef<number>(0);
const hasTriggeredThisSegment = useRef<boolean>(false); const hasTriggeredThisSegment = useRef<boolean>(false);
const footstepAudio = useRef<HTMLAudioElement[]>([]); const footstepAudio = useRef<HTMLAudioElement[]>([]);
const hasStepped = useRef<boolean>(false); const hasStepped = useRef<boolean>(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(() => { const randomIndex = Math.floor(
playerRoot.set(camera.position.x, FEAR_SETTINGS.PLAYER_HEIGHT, camera.position.z); Math.random() * footstepAudio.current.length
footstepAudio.current = Array.from({ length: 6 }, (_, i) => { );
const audio = new Audio(`fear/snd/footstep${i + 1}.mp3`); const audio = footstepAudio.current[randomIndex];
audio.volume = 0.4;
return audio;
});
}, []);
const playRandomFootstep = () => { audio.currentTime = 0;
if (footstepAudio.current.length === 0) return; audio.play().catch((err) => {
console.warn(
'Footstep playback blocked by browser autocomplete/interaction rules.',
err
);
});
};
const randomIndex = Math.floor(Math.random() * footstepAudio.current.length); useFrame((state, delta) => {
const audio = footstepAudio.current[randomIndex]; const dt = Math.min(delta, 0.1);
audio.currentTime = 0; camera.getWorldDirection(forward);
audio.play().catch((err) => { forward.y = 0;
console.warn("Footstep playback blocked by browser autocomplete/interaction rules.", err); forward.normalize();
}); side.crossVectors(forward, THREE.Object3D.DEFAULT_UP).normalize();
};
useFrame((state, delta) => { const moveForward = Number(controls.Forward) - Number(controls.Backward);
const dt = Math.min(delta, 0.1); const moveSide = Number(controls.Right) - Number(controls.Left);
camera.getWorldDirection(forward); targetVelocity.set(0, 0, 0);
forward.y = 0; if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward);
forward.normalize(); if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide);
side.crossVectors(forward, THREE.Object3D.DEFAULT_UP).normalize();
const moveForward = Number(controls.Forward) - Number(controls.Backward); if (targetVelocity.lengthSq() > 0)
const moveSide = Number(controls.Right) - Number(controls.Left); targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED);
targetVelocity.set(0, 0, 0); currentVelocity.lerp(targetVelocity, 10 * dt);
if (moveForward !== 0) targetVelocity.addScaledVector(forward, moveForward);
if (moveSide !== 0) targetVelocity.addScaledVector(side, moveSide);
if (targetVelocity.lengthSq() > 0) playerRoot.x += currentVelocity.x * dt;
targetVelocity.normalize().multiplyScalar(FEAR_SETTINGS.PLAYER_SPEED); 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; const isMoving =
playerRoot.z += currentVelocity.z * dt; controls.Forward || controls.Backward || controls.Left || controls.Right;
const minX = -fearState.currentWidth / 2 + FEAR_SETTINGS.WALL_BUFFER; bobIntensity.current = THREE.MathUtils.lerp(
const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER; bobIntensity.current,
playerRoot.x = THREE.MathUtils.clamp(playerRoot.x, minX, maxX); 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) if (isMoving && sinWave < -0.9) {
movementCounter.current += dt * 12; if (!hasStepped.current) {
playRandomFootstep();
hasStepped.current = true;
}
} else if (sinWave > 0) {
hasStepped.current = false;
}
const sinWave = Math.sin(movementCounter.current); const breatheTime = state.clock.elapsedTime * 1.8;
const moveBobY = sinWave * 0.06 * bobIntensity.current; const breatheBobY =
const moveBobX = Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current; Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5);
if (isMoving && sinWave < -0.9) { camera.position.copy(playerRoot);
if (!hasStepped.current) { camera.position.y += moveBobY + breatheBobY;
playRandomFootstep(); camera.position.addScaledVector(side, moveBobX);
hasStepped.current = true;
}
} else if (sinWave > 0) {
hasStepped.current = false;
}
const breatheTime = state.clock.elapsedTime * 1.8; if (flashlightRef.current) {
const breatheBobY = Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5); flashlightRef.current.position.lerp(camera.position, 7 * dt);
camera.getWorldDirection(viewDirection);
camera.position.copy(playerRoot); targetDest.copy(camera.position).addScaledVector(viewDirection, 10);
camera.position.y += moveBobY + breatheBobY;
camera.position.addScaledVector(side, moveBobX);
if (flashlightRef.current) { flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
flashlightRef.current.position.lerp(camera.position, 7 * dt); flashlightRef.current.target.updateMatrixWorld();
camera.getWorldDirection(viewDirection);
targetDest flashlightRef.current.intensity =
.copy(camera.position) FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
.addScaledVector(viewDirection, 10); Math.sin(state.clock.elapsedTime * 30) *
0.15 *
Math.cos(state.clock.elapsedTime * 3);
}
flashlightRef.current.target.position.lerp(targetDest, 12 * dt); const length = FEAR_SETTINGS.HALLWAY_LENGTH;
flashlightRef.current.target.updateMatrixWorld(); const absoluteZ = -playerRoot.z;
const rawSegmentIndex = Math.floor(absoluteZ / length);
const progressZ = (((absoluteZ % length) + length) % length) / length;
flashlightRef.current.intensity = if (rawSegmentIndex > confirmedSegment.current && progressZ > 0.25) {
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE + if (!hasTriggeredThisSegment.current) {
Math.sin(state.clock.elapsedTime * 30) * 0.15 * Math.cos(state.clock.elapsedTime * 3); 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; if (
const absoluteZ = -playerRoot.z; rawSegmentIndex === confirmedSegment.current &&
const rawSegmentIndex = Math.floor(absoluteZ / length); progressZ > 0.35 &&
const progressZ = ((absoluteZ % length) + length) % length / length; progressZ < 0.65
) {
hasTriggeredThisSegment.current = false;
}
});
if (rawSegmentIndex > confirmedSegment.current && progressZ > 0.25) { return (
if (!hasTriggeredThisSegment.current) { <>
fearState.registerLoop('forward'); <PointerLockControls />
hasTriggeredThisSegment.current = true; <spotLight
} ref={flashlightRef}
confirmedSegment.current = rawSegmentIndex; distance={25}
} angle={0.35}
else if (rawSegmentIndex < confirmedSegment.current && progressZ < 0.75) { penumbra={0.8}
if (!hasTriggeredThisSegment.current) { intensity={0}
fearState.registerLoop('backward'); color='#fffaed'
hasTriggeredThisSegment.current = true; decay={2}
} castShadow
confirmedSegment.current = rawSegmentIndex; shadow-bias={-0.001}
} />
</>
if (rawSegmentIndex === confirmedSegment.current && progressZ > 0.35 && progressZ < 0.65) { );
hasTriggeredThisSegment.current = false; }
}
});
return (
<>
<PointerLockControls />
<spotLight
ref={flashlightRef}
distance={25}
angle={0.35}
penumbra={0.8}
intensity={0}
color="#fffaed"
decay={2}
castShadow
shadow-bias={-0.001}
/>
</>
);
}
+15 -11
View File
@@ -1,5 +1,9 @@
export function ShaderPatch(shader: { vertexShader: string, fragmentShader: string, uniforms: Object }) { export function ShaderPatch(shader: {
shader.vertexShader = ` vertexShader: string;
fragmentShader: string;
uniforms: Object;
}) {
shader.vertexShader = `
varying float vDepth; varying float vDepth;
#ifdef USE_MAP #ifdef USE_MAP
varying vec2 vAffineUv; varying vec2 vAffineUv;
@@ -7,9 +11,9 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri
${shader.vertexShader} ${shader.vertexShader}
`; `;
shader.vertexShader = shader.vertexShader.replace( shader.vertexShader = shader.vertexShader.replace(
`#include <project_vertex>`, `#include <project_vertex>`,
` `
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition; gl_Position = projectionMatrix * mvPosition;
@@ -24,9 +28,9 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri
vAffineUv = vMapUv * gl_Position.w; vAffineUv = vMapUv * gl_Position.w;
#endif #endif
` `
); );
shader.fragmentShader = ` shader.fragmentShader = `
varying float vDepth; varying float vDepth;
#ifdef USE_MAP #ifdef USE_MAP
varying vec2 vAffineUv; varying vec2 vAffineUv;
@@ -34,9 +38,9 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri
${shader.fragmentShader} ${shader.fragmentShader}
`; `;
shader.fragmentShader = shader.fragmentShader.replace( shader.fragmentShader = shader.fragmentShader.replace(
`#include <map_fragment>`, `#include <map_fragment>`,
` `
#ifdef USE_MAP #ifdef USE_MAP
vec2 flatAffineUV = vAffineUv / max(vDepth, 0.001); vec2 flatAffineUV = vAffineUv / max(vDepth, 0.001);
@@ -53,5 +57,5 @@ export function ShaderPatch(shader: { vertexShader: string, fragmentShader: stri
diffuseColor *= texelColor; diffuseColor *= texelColor;
#endif #endif
` `
); );
} }
+65 -53
View File
@@ -1,74 +1,86 @@
import * as THREE from 'three'; import * as THREE from 'three';
export const FEAR_SETTINGS = { export const FEAR_SETTINGS = {
HALLWAY_LENGTH: 40, HALLWAY_LENGTH: 40,
HALLWAY_WIDTH: 6, HALLWAY_WIDTH: 6,
HALLWAY_HEIGHT: 5, HALLWAY_HEIGHT: 5,
PLAYER_HEIGHT: 3, PLAYER_HEIGHT: 3,
PLAYER_SPEED: 4, PLAYER_SPEED: 4,
FLASHLIGHT_INTENSITY_BASE: 8, FLASHLIGHT_INTENSITY_BASE: 8,
WALL_BUFFER: 0.6, WALL_BUFFER: 0.6,
CREATURE_SPEED: 8, CREATURE_SPEED: 8,
EVENT_NARROW_LOOP_COUNT: 2, EVENT_NARROW_LOOP_COUNT: 2,
EVENT_RUST_LOOP_COUNT: 4, EVENT_RUST_LOOP_COUNT: 4,
EVENT_FINALE_LOOP_COUNT: 5, EVENT_FINALE_LOOP_COUNT: 5,
EVENT_FINALE_DURATION: 1, EVENT_FINALE_DURATION: 1,
TEST_MODE: false TEST_MODE: false
}; };
const listeners = new Set<() => void>(); const listeners = new Set<() => void>();
export const fearState = { export const fearState = {
loopCount: 0, loopCount: 0,
currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH, currentWidth: FEAR_SETTINGS.HALLWAY_WIDTH,
isRustActive: false, isRustActive: false,
finaleTriggered: false, finaleTriggered: false,
wasCaught: false, wasCaught: false,
finaleProgression: 0, finaleProgression: 0,
subscribe(listener: () => void) { subscribe(listener: () => void) {
listeners.add(listener); listeners.add(listener);
return () => { listeners.delete(listener); }; return () => {
}, listeners.delete(listener);
};
},
emit() { emit() {
listeners.forEach((listener) => listener()); listeners.forEach((listener) => listener());
}, },
update(delta: number) { update(delta: number) {
const targetWidth = this.loopCount >= FEAR_SETTINGS.EVENT_NARROW_LOOP_COUNT ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH; const targetWidth =
const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta); 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) { if (Math.abs(this.currentWidth - newWidth) > 0.001) {
this.currentWidth = newWidth; this.currentWidth = newWidth;
} }
if (this.wasCaught) { if (this.wasCaught) {
if (this.finaleProgression < FEAR_SETTINGS.EVENT_FINALE_DURATION) { 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); this.emit();
} else { },
window.location.href = '/';
}
}
this.emit(); registerLoop(direction: 'forward' | 'backward') {
}, this.loopCount += 1;
registerLoop(direction: 'forward' | 'backward') { this.isRustActive = this.loopCount >= FEAR_SETTINGS.EVENT_RUST_LOOP_COUNT;
this.loopCount += 1; this.finaleTriggered =
this.loopCount >= FEAR_SETTINGS.EVENT_FINALE_LOOP_COUNT;
this.isRustActive = this.loopCount >= FEAR_SETTINGS.EVENT_RUST_LOOP_COUNT; this.emit();
this.finaleTriggered = this.loopCount >= FEAR_SETTINGS.EVENT_FINALE_LOOP_COUNT; },
this.emit(); registerCaught() {
}, this.wasCaught = true;
this.emit();
registerCaught() { }
this.wasCaught = true; };
this.emit();
}
};
+1 -1
View File
@@ -43,4 +43,4 @@
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
+150 -138
View File
@@ -2,10 +2,21 @@
import './page.css'; 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 { Canvas, useLoader } from '@react-three/fiber';
import { Bloom, BrightnessContrast, DepthOfField, EffectComposer, HueSaturation, LUT, Noise, SMAA, SSAO, Vignette } from '@react-three/postprocessing'; import {
import { useLayoutEffect, useState } from "react"; 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 { folder, useControls, Leva } from 'leva';
import SealCube from './scene-components/sealcube'; import SealCube from './scene-components/sealcube';
import Terrain from './scene-components/terrain'; 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'; import { AmbientSound } from './scene-components/ambient-sound';
function Loader() { function Loader() {
const { progress, active } = useProgress(); const { progress, active } = useProgress();
const [visible, setVisible] = useState(true); const [visible, setVisible] = useState(true);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!active && progress === 100) { if (!active && progress === 100) {
const timeout = setTimeout(() => setVisible(false), 500); const timeout = setTimeout(() => setVisible(false), 500);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
} }
}, [progress, active]); }, [progress, active]);
return ( return (
<div className={`loader ${!visible ? 'loader hidden' : ''}`}> <div className={`loader ${!visible ? 'loader hidden' : ''}`}>
<picture> <picture>
<img src='niko/img/niko.jpg' className='niko-spin' /> <img src='niko/img/niko.jpg' className='niko-spin' />
</picture> </picture>
</div> </div>
); );
} }
function Scene() { function Scene() {
const { const {
terrainDryColor, terrainDryColor,
terrainLushColor, terrainLushColor,
chunks, chunks,
chunkSize, chunkSize,
resolution, resolution,
hillScale, hillScale,
hillHeight, hillHeight,
detailScale, detailScale,
detailHeight, detailHeight,
grassDryColor, grassDryColor,
grassLushColor, grassLushColor,
grassCount, grassCount,
grassSize, grassSize,
grassLOD, grassLOD,
grassBlades, grassBlades,
grassSegments, grassSegments,
grassLODStart, grassLODStart,
grassLODExponent grassLODExponent
} = useControls('Environment', { } = useControls('Environment', {
Terrain: folder({ Terrain: folder({
terrainDryColor: '#232a0c', terrainDryColor: '#232a0c',
terrainLushColor: '#142a14', terrainLushColor: '#142a14',
chunks: { value: 16, min: 4, max: 24, step: 2 }, chunks: { value: 16, min: 4, max: 24, step: 2 },
chunkSize: { value: 10.0, min: 5.0, max: 40.0, step: 1.0 }, 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 }, 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 }, 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 }, 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 }, 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 }, detailHeight: { value: 0.3, min: 0.0, max: 2.0, step: 0.05 }
}), }),
Grass: folder({ Grass: folder({
grassDryColor: '#495a17', grassDryColor: '#495a17',
grassLushColor: '#255825', grassLushColor: '#255825',
grassCount: { value: 8000, min: 1000, max: 30000, step: 500 }, grassCount: { value: 8000, min: 1000, max: 30000, step: 500 },
grassSize: { value: 0.85, min: 0.1, max: 2.0, step: 0.05 }, grassSize: { value: 0.85, min: 0.1, max: 2.0, step: 0.05 },
grassLOD: { value: 60, min: 10, max: 200, step: 5 }, grassLOD: { value: 60, min: 10, max: 200, step: 5 },
grassBlades: { value: 3, min: 1, max: 5, step: 1 }, grassBlades: { value: 3, min: 1, max: 5, step: 1 },
grassSegments: { value: 4, 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 }, 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 }, grassLODExponent: { value: 1.8, min: 0.5, max: 3.0, step: 0.1 }
}) })
}); });
return (<> return (
<Environment <>
files={'niko/hdr/sky.hdr'} <Environment
environmentIntensity={0.85} files={'niko/hdr/sky.hdr'}
background environmentIntensity={0.85}
/> background
/>
<fogExp2 attach='fog' args={[0xa3a5ba, 0.0125]} /> <fogExp2 attach='fog' args={[0xa3a5ba, 0.0125]} />
<ambientLight intensity={0.5} /> <ambientLight intensity={0.5} />
<directionalLight <directionalLight position={[15, 25, 15]} intensity={1} />
position={[15, 25, 15]}
intensity={1}
/>
<Terrain <Terrain
chunks={chunks} chunks={chunks}
chunkSize={chunkSize} chunkSize={chunkSize}
resolution={resolution} resolution={resolution}
scale={1} scale={1}
hillScale={hillScale} hillScale={hillScale}
hillHeight={hillHeight} hillHeight={hillHeight}
detailScale={detailScale} detailScale={detailScale}
detailHeight={detailHeight} detailHeight={detailHeight}
grassCount={grassCount} grassCount={grassCount}
grassSize={grassSize} grassSize={grassSize}
grassLOD={grassLOD} grassLOD={grassLOD}
terrainDryColor={terrainDryColor} terrainDryColor={terrainDryColor}
terrainLushColor={terrainLushColor} terrainLushColor={terrainLushColor}
grassDryColor={grassDryColor} grassDryColor={grassDryColor}
grassLushColor={grassLushColor} grassLushColor={grassLushColor}
grassBlades={grassBlades} grassBlades={grassBlades}
grassSegments={grassSegments} grassSegments={grassSegments}
grassLODStart={grassLODStart} grassLODStart={grassLODStart}
grassLODExponent={grassLODExponent} grassLODExponent={grassLODExponent}
/> />
<SealCube /> <SealCube />
</>) </>
);
} }
function LutEffect() { function LutEffect() {
const lutTexture = useLoader(LUTCubeLoader, 'niko/lut/Landscape6.cube'); const lutTexture = useLoader(LUTCubeLoader, 'niko/lut/Landscape6.cube');
return <LUT lut={lutTexture.texture3D} />; return <LUT lut={lutTexture.texture3D} />;
} }
function PostProcessing() { function PostProcessing() {
return (<EffectComposer> return (
<DepthOfField target={[0, 3, 0]} focalLength={10} bokehScale={5} /> <EffectComposer>
<Vignette /> <DepthOfField target={[0, 3, 0]} focalLength={10} bokehScale={5} />
<Noise opacity={0.05} /> <Vignette />
<Bloom <Noise opacity={0.05} />
intensity={0.8} <Bloom
luminanceThreshold={0.4} intensity={0.8}
luminanceSmoothing={0.5} luminanceThreshold={0.4}
/> luminanceSmoothing={0.5}
<SMAA /> />
<HueSaturation saturation={0.3} /> <SMAA />
<BrightnessContrast brightness={0.05} contrast={-0.1} /> <HueSaturation saturation={0.3} />
<LutEffect /> <BrightnessContrast brightness={0.05} contrast={-0.1} />
</EffectComposer>) <LutEffect />
</EffectComposer>
);
} }
export default function Seal() { export default function Seal() {
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
return ( return (
<> <>
<Leva hidden={isProduction} /> <Leva hidden={isProduction} />
<Loader /> <Loader />
<Canvas <Canvas
shadows shadows
camera={{ position: [0, 5, 15], fov: 50, far: 100 }} camera={{ position: [0, 5, 15], fov: 50, far: 100 }}
gl={{ antialias: false, powerPreference: "high-performance" }} gl={{ antialias: false, powerPreference: 'high-performance' }}
className='canvas' className='canvas'
> >
<AmbientSound url="niko/snd/wind.mp3" volume={0.4} /> <AmbientSound url='niko/snd/wind.mp3' volume={0.4} />
<AmbientSound url="niko/snd/birds.mp3" volume={0.1} /> <AmbientSound url='niko/snd/birds.mp3' volume={0.1} />
<Scene /> <Scene />
<PostProcessing /> <PostProcessing />
<OrbitControls <OrbitControls
target={[0, 3, 0]} target={[0, 3, 0]}
enablePan={false} enablePan={false}
makeDefault makeDefault
minDistance={2} minDistance={2}
maxDistance={6} maxDistance={6}
/> />
</Canvas> </Canvas>
</> </>
); );
} }
+31 -31
View File
@@ -1,43 +1,43 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react';
interface AmbientSoundProps { interface AmbientSoundProps {
url: string url: string;
volume?: number volume?: number;
} }
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) { export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
const audioRef = useRef<HTMLAudioElement | null>(null) const audioRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => { useEffect(() => {
const audio = new Audio(url) const audio = new Audio(url);
audio.loop = true audio.loop = true;
audio.volume = volume audio.volume = volume;
audioRef.current = audio audioRef.current = audio;
const startAudio = () => { const startAudio = () => {
audio.play().catch((err) => { audio.play().catch((err) => {
console.warn('Autoplay blocked. Waiting for user interaction.', err) console.warn('Autoplay blocked. Waiting for user interaction.', err);
}) });
} };
startAudio() startAudio();
window.addEventListener('click', startAudio, { once: true }) window.addEventListener('click', startAudio, { once: true });
window.addEventListener('keydown', startAudio, { once: true }) window.addEventListener('keydown', startAudio, { once: true });
return () => { return () => {
window.removeEventListener('click', startAudio) window.removeEventListener('click', startAudio);
window.removeEventListener('keydown', startAudio) window.removeEventListener('keydown', startAudio);
audio.pause() audio.pause();
audioRef.current = null audioRef.current = null;
} };
}, [url]) }, [url]);
useEffect(() => { useEffect(() => {
if (audioRef.current) { if (audioRef.current) {
audioRef.current.volume = volume audioRef.current.volume = volume;
} }
}, [volume]) }, [volume]);
return null return null;
} }
+16 -6
View File
@@ -1,7 +1,16 @@
import { useFrame, useLoader } from "@react-three/fiber"; import { useFrame, useLoader } from '@react-three/fiber';
import { useLayoutEffect, useMemo, useRef } from "react"; import { useLayoutEffect, useMemo, useRef } from 'react';
import { BufferAttribute, BufferGeometry, Color, DoubleSide, InstancedMesh, MeshStandardMaterial, Object3D, TextureLoader } from "three"; import {
import { getTerrainHeight, Shader } from "./helpers"; BufferAttribute,
BufferGeometry,
Color,
DoubleSide,
InstancedMesh,
MeshStandardMaterial,
Object3D,
TextureLoader
} from 'three';
import { getTerrainHeight, Shader } from './helpers';
import grassVert from './shaders/grass.vert'; import grassVert from './shaders/grass.vert';
import grassFrag from './shaders/grass.frag'; import grassFrag from './shaders/grass.frag';
@@ -187,7 +196,8 @@ export default function Grass({
const perBladeRandom = Math.random() * 0.4; const perBladeRandom = Math.random() * 0.4;
const grassWidth = grassSize * (0.7 + Math.random() * 0.5); const grassWidth = grassSize * (0.7 + Math.random() * 0.5);
const grassHeight = grassSize * (0.4 + macroHeight * 0.8 + microHeight + perBladeRandom); const grassHeight =
grassSize * (0.4 + macroHeight * 0.8 + microHeight + perBladeRandom);
dummy.scale.set(grassWidth, grassHeight, grassWidth); dummy.scale.set(grassWidth, grassHeight, grassWidth);
@@ -309,4 +319,4 @@ export default function Grass({
/> />
</instancedMesh> </instancedMesh>
); );
} }
+17 -17
View File
@@ -1,24 +1,24 @@
export function getTerrainHeight( export function getTerrainHeight(
localX: number, localX: number,
localZ: number, localZ: number,
worldXBase: number, worldXBase: number,
worldZBase: number, worldZBase: number,
scale: number, scale: number,
hillScale: number, hillScale: number,
hillHeight: number, hillHeight: number,
detailScale: number, detailScale: number,
detailHeight: number, detailHeight: number,
noise2D: (x: number, y: number) => number noise2D: (x: number, y: number) => number
) { ) {
const worldX = (worldXBase + localX) * 0.1; const worldX = (worldXBase + localX) * 0.1;
const worldZ = (worldZBase + localZ) * 0.1; const worldZ = (worldZBase + localZ) * 0.1;
const noiseHill = const noiseHill =
noise2D(worldX * hillScale, worldZ * hillScale) * hillHeight; noise2D(worldX * hillScale, worldZ * hillScale) * hillHeight;
const noiseDetail = const noiseDetail =
noise2D(worldX * detailScale, worldZ * detailScale) * detailHeight; noise2D(worldX * detailScale, worldZ * detailScale) * detailHeight;
return (noiseHill + noiseDetail) * scale; return (noiseHill + noiseDetail) * scale;
} }
export interface Shader { export interface Shader {
+26 -25
View File
@@ -1,33 +1,34 @@
import { useFrame, useLoader } from "@react-three/fiber"; import { useFrame, useLoader } from '@react-three/fiber';
import { forwardRef, useImperativeHandle, useRef } from "react"; import { forwardRef, useImperativeHandle, useRef } from 'react';
import { Mesh, TextureLoader } from "three"; import { Mesh, TextureLoader } from 'three';
const SealCube = forwardRef<Mesh>((props, ref) => { const SealCube = forwardRef<Mesh>((props, ref) => {
const texture = useLoader(TextureLoader, 'niko/img/niko.jpg'); const texture = useLoader(TextureLoader, 'niko/img/niko.jpg');
const meshRef = useRef<Mesh>(null); const meshRef = useRef<Mesh>(null);
useImperativeHandle(ref, () => meshRef.current!, []); useImperativeHandle(ref, () => meshRef.current!, []);
useFrame((state, delta) => { useFrame((state, delta) => {
if (meshRef.current) { if (meshRef.current) {
meshRef.current.rotation.x += delta * 0.5; meshRef.current.rotation.x += delta * 0.5;
meshRef.current.rotation.y += delta * 0.5; meshRef.current.rotation.y += delta * 0.5;
meshRef.current.position.y = 3 + Math.sin(state.clock.getElapsedTime() * 1) * 0.15; meshRef.current.position.y =
} 3 + Math.sin(state.clock.getElapsedTime() * 1) * 0.15;
}); }
});
return ( return (
<mesh ref={meshRef} position={[0, 3, 0]} castShadow receiveShadow> <mesh ref={meshRef} position={[0, 3, 0]} castShadow receiveShadow>
<boxGeometry args={[0.85, 0.85, 0.85]} /> <boxGeometry args={[0.85, 0.85, 0.85]} />
<meshStandardMaterial <meshStandardMaterial
map={texture} map={texture}
roughness={0.4} roughness={0.4}
metalness={0.1} metalness={0.1}
envMapIntensity={1.2} envMapIntensity={1.2}
/> />
</mesh> </mesh>
); );
}); });
SealCube.displayName = 'SealCube'; SealCube.displayName = 'SealCube';
export default SealCube; export default SealCube;
+10 -8
View File
@@ -1,8 +1,8 @@
import { useMemo, useRef } from "react"; import { useMemo, useRef } from 'react';
import { BufferAttribute, BufferGeometry, Color, Mesh } from "three"; import { BufferAttribute, BufferGeometry, Color, Mesh } from 'three';
import { getTerrainHeight } from "./helpers"; import { getTerrainHeight } from './helpers';
import Grass from "./grass"; import Grass from './grass';
import { createNoise2D } from "simplex-noise"; import { createNoise2D } from 'simplex-noise';
interface TerrainChunkProps { interface TerrainChunkProps {
x: number; x: number;
@@ -54,8 +54,10 @@ function TerrainChunk({
const worldXBase = x * size; const worldXBase = x * size;
const worldZBase = y * size; const worldZBase = y * size;
const minX = Math.abs(worldXBase) <= halfSize ? 0 : Math.abs(worldXBase) - halfSize; const minX =
const minZ = Math.abs(worldZBase) <= halfSize ? 0 : Math.abs(worldZBase) - halfSize; Math.abs(worldXBase) <= halfSize ? 0 : Math.abs(worldXBase) - halfSize;
const minZ =
Math.abs(worldZBase) <= halfSize ? 0 : Math.abs(worldZBase) - halfSize;
const chunkMinDist = Math.sqrt(minX * minX + minZ * minZ); const chunkMinDist = Math.sqrt(minX * minX + minZ * minZ);
const shouldRenderGrass = chunkMinDist < grassLOD; const shouldRenderGrass = chunkMinDist < grassLOD;
@@ -276,4 +278,4 @@ export default function Terrain({
))} ))}
</group> </group>
); );
} }
+28 -11
View File
@@ -22,12 +22,14 @@
} }
* { * {
cursor: url('/cur/kuromi.webp') 32 32, auto; cursor:
url('/cur/kuromi.webp') 32 32,
auto;
} }
body { body {
color: var(--text-main); color: var(--text-main);
font-family: "Times New Roman", Times, serif; font-family: 'Times New Roman', Times, serif;
text-align: center; text-align: center;
display: flex; display: flex;
@@ -68,7 +70,9 @@ body {
border: 1px solid var(--pink-accent); border: 1px solid var(--pink-accent);
outline: 4px solid #fff; outline: 4px solid #fff;
box-shadow: 0 0 0 5px var(--accent), 8px 8px 0px rgba(215, 230, 255, 0.5); box-shadow:
0 0 0 5px var(--accent),
8px 8px 0px rgba(215, 230, 255, 0.5);
border-radius: 2px; border-radius: 2px;
animation: float 5s ease-in-out infinite; animation: float 5s ease-in-out infinite;
@@ -79,14 +83,20 @@ body {
top: 10px; top: 10px;
font-size: 1.5rem; font-size: 1.5rem;
color: var(--sparkle); color: var(--sparkle);
text-shadow: 0 0 20px var(--sparkle-glow), 0 0 15px var(--sparkle-glow), 0 0 10px var(--sparkle-glow), 0 0 5px var(--sparkle-glow); text-shadow:
0 0 20px var(--sparkle-glow),
0 0 15px var(--sparkle-glow),
0 0 10px var(--sparkle-glow),
0 0 5px var(--sparkle-glow);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
line-height: 1; line-height: 1;
height: 24px; height: 24px;
width: 24px; width: 24px;
cursor: url('/cur/kuromi-hearts.webp') 0 0, auto !important; cursor:
url('/cur/kuromi-hearts.webp') 0 0,
auto !important;
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
@@ -104,7 +114,7 @@ body {
} }
.content-box::before { .content-box::before {
content: "•"; content: '•';
position: absolute; position: absolute;
top: -4px; top: -4px;
left: 6px; left: 6px;
@@ -204,15 +214,17 @@ h1 {
} }
.directory a::before { .directory a::before {
content: " " content: ' ';
} }
.directory a::after { .directory a::after {
content: " " content: ' ';
} }
.directory a:hover { .directory a:hover {
cursor: url('/cur/kuromi-hearts.webp') 0 0, auto !important; cursor:
url('/cur/kuromi-hearts.webp') 0 0,
auto !important;
color: var(--text-header); color: var(--text-header);
box-shadow: 1px 1px 0px var(--pink-accent); box-shadow: 1px 1px 0px var(--pink-accent);
background: var(--pink-accent); background: var(--pink-accent);
@@ -234,7 +246,12 @@ h1 {
border-top: 1px double var(--pink-accent); border-top: 1px double var(--pink-accent);
border-bottom: 1px double var(--pink-accent); border-bottom: 1px double var(--pink-accent);
background: linear-gradient(to right, transparent, rgba(255, 214, 245, 0.3), transparent); background: linear-gradient(
to right,
transparent,
rgba(255, 214, 245, 0.3),
transparent
);
} }
.marquee-track { .marquee-track {
@@ -310,4 +327,4 @@ h1 {
cursor: pointer; cursor: pointer;
text-decoration: underline; text-decoration: underline;
text-underline-offset: 2px; text-underline-offset: 2px;
} }
+121 -47
View File
@@ -4,10 +4,10 @@ import { useEffect, useState } from 'react';
import './page.css'; import './page.css';
import { DiscordStatus } from './components/discordstatus'; import { DiscordStatus } from './components/discordstatus';
const TWITTER_LINK = "https://x.com/neruu444" const TWITTER_LINK = 'https://x.com/neruu444';
const DISCORD_USER = "neru444" const DISCORD_USER = 'neru444';
const DISCORD_ID = "1104474057916809226" const DISCORD_ID = '1104474057916809226';
const STEAM_LINK = "https://steamcommunity.com/profiles/76561198440714757/" const STEAM_LINK = 'https://steamcommunity.com/profiles/76561198440714757/';
function Content() { function Content() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -16,66 +16,136 @@ function Content() {
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) if (e.key === 'Escape' && isOpen) setIsOpen(false);
setIsOpen(false);
}; };
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen]); }, [isOpen]);
const marqueeText = "✧ ꒰ა˵• ﻌ •˵ა꒱ ✧ ฅ^•ﻌ•^ฅ ✧ ᶻ 𝗓 𐰁 /ᐠ. 。 .ᐟ\\ ✧ ฅ/ᐠ. ̫ .ᐟ\\ฅ ✧ ꒰ა≽^•⩊•^≼໒꒱ ✧ ₍˄·͈༝·͈˄₎ ✧ /ᐠ. ⩊ .ᐟ\\ノ ✧ 𓏲ּ ֶָ ࣪ /ᐠ .ᆺ. ᐟ\\ノ ✧"; const marqueeText =
'✧ ꒰ა˵• ﻌ •˵ა꒱ ✧ ฅ^•ﻌ•^ฅ ✧ ᶻ 𝗓 𐰁 /ᐠ. 。 .ᐟ\\ ✧ ฅ/ᐠ. ̫ .ᐟ\\ฅ ✧ ꒰ა≽^•⩊•^≼໒꒱ ✧ ₍˄·͈༝·͈˄₎ ✧ /ᐠ. ⩊ .ᐟ\\ノ ✧ 𓏲ּ ֶָ ࣪ /ᐠ .ᆺ. ᐟ\\ノ ✧';
return ( return (
<> <>
<div className="main-frame"> <div className='main-frame'>
<a href="/niko" className="decorative-sparkle" title="✧" style={{ left: '10px' }}></a> <a
<a href="/fear" className="decorative-sparkle" title="✧" style={{ right: '10px' }}></a> href='/niko'
className='decorative-sparkle'
title='✧'
style={{ left: '10px' }}
>
</a>
<a
href='/fear'
className='decorative-sparkle'
title='✧'
style={{ right: '10px' }}
>
</a>
<header> <header>
<h1>neru</h1> <h1>neru</h1>
<p className="motto">˚ 𓂋 ˚</p> <p className='motto'>˚ 𓂋 ˚</p>
</header> </header>
<nav className="social-links"> <nav className='social-links'>
<a href={TWITTER_LINK} target="_blank" rel="noopener noreferrer">twitter</a> <a href={TWITTER_LINK} target='_blank' rel='noopener noreferrer'>
<button onClick={toggleModal}>discord</button> twitter
<a href={STEAM_LINK} target="_blank" rel="noopener noreferrer">steam</a> </a>{' '}
<button onClick={toggleModal}>discord</button>
<a href={STEAM_LINK} target='_blank' rel='noopener noreferrer'>
steam
</a>
</nav> </nav>
<section className="content-box"> <section className='content-box'>
<h2 className="title"> discord </h2> <h2 className='title'> discord </h2>
<DiscordStatus userId={DISCORD_ID} /> <DiscordStatus userId={DISCORD_ID} />
</section> </section>
<section className="content-box"> <section className='content-box'>
<h2 className="title"> projects im currently working on </h2> <h2 className='title'> projects im currently working on </h2>
<ul className="directory"> <ul className='directory'>
<li><a href="https://git.neru.rip/neru/seallib" target="_blank" rel="noopener noreferrer">seallib</a></li> <li>
<li><a href="https://git.neru.rip/neru/tinymitm" target="_blank" rel="noopener noreferrer">tinymitm</a></li> <a
<li><a href="https://git.neru.rip/neru/luma" target="_blank" rel="noopener noreferrer">luma</a></li> href='https://git.neru.rip/neru/seallib'
target='_blank'
rel='noopener noreferrer'
>
seallib
</a>
</li>
<li>
<a
href='https://git.neru.rip/neru/tinymitm'
target='_blank'
rel='noopener noreferrer'
>
tinymitm
</a>
</li>
<li>
<a
href='https://git.neru.rip/neru/luma'
target='_blank'
rel='noopener noreferrer'
>
luma
</a>
</li>
</ul> </ul>
</section> </section>
<section className="content-box"> <section className='content-box'>
<h2 className="title"> sites </h2> <h2 className='title'> sites </h2>
<ul className="directory"> <ul className='directory'>
<li><a href="https://git.neru.rip" target="_blank" rel="noopener noreferrer">gitea</a></li> <li>
<li><a href="https://zl.neru.rip" target="_blank" rel="noopener noreferrer">zipline</a></li> <a
<li><a href="https://files.neru.rip" target="_blank" rel="noopener noreferrer">files</a></li> href='https://git.neru.rip'
target='_blank'
rel='noopener noreferrer'
>
gitea
</a>
</li>
<li>
<a
href='https://zl.neru.rip'
target='_blank'
rel='noopener noreferrer'
>
zipline
</a>
</li>
<li>
<a
href='https://files.neru.rip'
target='_blank'
rel='noopener noreferrer'
>
files
</a>
</li>
</ul> </ul>
</section> </section>
<section className="content-box"> <section className='content-box'>
<h2 className="title"> dumb stuff </h2> <h2 className='title'> dumb stuff </h2>
<ul className="directory"> <ul className='directory'>
<li><a href="discord://-/apps">break discord</a></li> <li>
<li><a href="discord://-/channels/@me/">fix discord</a></li> <a href='discord://-/apps'>break discord</a>
</li>
<li>
<a href='discord://-/channels/@me/'>fix discord</a>
</li>
</ul> </ul>
</section> </section>
<footer> <footer>
<div className="marquee"> <div className='marquee'>
<div className="marquee-track"> <div className='marquee-track'>
<span>{marqueeText}</span> <span>{marqueeText}</span>
<span>{marqueeText}</span> <span>{marqueeText}</span>
</div> </div>
@@ -83,14 +153,20 @@ function Content() {
</footer> </footer>
{isOpen && ( {isOpen && (
<div className="modal-overlay" onClick={toggleModal}> <div className='modal-overlay' onClick={toggleModal}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}> <div className='modal-content' onClick={(e) => e.stopPropagation()}>
<div className="modal-header"> discord info </div> <div className='modal-header'> discord info </div>
<div className="modal-body"> <div className='modal-body'>
<p><strong>User:</strong> {DISCORD_USER}</p> <p>
<p><strong>ID:</strong> {DISCORD_ID}</p> <strong>User:</strong> {DISCORD_USER}
</p>
<p>
<strong>ID:</strong> {DISCORD_ID}
</p>
</div> </div>
<button className="modal-close-btn" onClick={toggleModal}>[ close ]</button> <button className='modal-close-btn' onClick={toggleModal}>
[ close ]
</button>
</div> </div>
</div> </div>
)} )}
@@ -100,7 +176,5 @@ function Content() {
} }
export default function Home() { export default function Home() {
return ( return <Content />;
<Content /> }
)
}