Compare commits

..

30 Commits

Author SHA1 Message Date
neru 97240373ca feat?: add placeholder under construction msg 2026-06-26 08:48:45 -03:00
neru eebd87a650 style: run format:apply 2026-06-26 08:48:31 -03:00
neru ab3bf047d4 style: reduce noise opacity 2026-06-02 04:43:52 -03:00
neru f1ab2b692d style: change text 2026-06-02 04:43:09 -03:00
neru 673aabce50 style: move patch to its own file 2026-06-02 04:42:16 -03:00
neru b3a5712c85 feat: make movement jitter, add phase effects 2026-06-02 04:42:07 -03:00
neru 4691a9fbf4 feat: add flicker anim 2026-06-02 04:30:03 -03:00
neru 0fca4db440 style: change size and alignment 2026-06-02 04:13:54 -03:00
neru 930139d1df feat: add unicode glitches 2026-06-02 04:13:29 -03:00
neru 4120e5ec72 feat: move href logic to state mgr 2026-06-02 03:57:42 -03:00
neru fd314cf2ec feat: make finale text infinite 2026-06-02 03:57:31 -03:00
neru e3ab974988 style: reduce fov 2026-06-02 03:45:04 -03:00
neru e4a0c57e79 feat: add TEST_MODE 2026-06-02 03:44:58 -03:00
neru ebda4b281e feat: add steel texture for doors 2026-06-02 03:29:17 -03:00
neru 8dcc888d5c feat: add psx style vertex snapping and affine distortion 2026-06-02 03:29:07 -03:00
neru b7e61b4240 feat: lerp flashlight pos 2026-06-02 02:50:11 -03:00
neru 23c39a71a6 feat: add footsteps 2026-06-02 02:50:04 -03:00
neru a0ee50703c feat: add better ambience sound 2026-06-02 02:44:44 -03:00
neru 8c4080f10c feat: add finale text 2026-06-01 22:02:22 -03:00
neru b9eeed848b feat: add door knocks 2026-06-01 22:02:12 -03:00
neru d506071ce2 feat: overhaul grass 2026-06-01 17:31:13 -03:00
neru cad47f07bd feat: randomize grass height 2026-06-01 17:02:05 -03:00
neru 10543bba89 fix: make grass rotation less strong 2026-06-01 17:00:33 -03:00
neru aeee2158ba feat/fix: misc shader changes (fix normals, lighting, uv) 2026-06-01 16:56:33 -03:00
neru 569a4f29fb fix: make fog fit hdr 2026-06-01 16:50:55 -03:00
neru 9a67a800fa feat: add hover anim 2026-06-01 16:49:04 -03:00
neru 719a75d393 feat: change cube lighting to standard mat 2026-06-01 16:49:00 -03:00
neru f583cfdc57 style: formatting 2026-06-01 16:47:44 -03:00
neru 5665804b8f fix: disable AA (already handled by SMAA) 2026-06-01 16:47:37 -03:00
neru 67bf6325fa fix: misc visual changes 2026-06-01 16:47:24 -03:00
36 changed files with 2120 additions and 1324 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16 -4
View File
@@ -31,10 +31,22 @@
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.online {
.status-dot.idle { background-color: #fef08a; border: 1px solid #facc15; } background-color: #a7f3d0;
.status-dot.dnd { background-color: #fecdd3; border: 1px solid #fb7185; } border: 1px solid #34d399;
.status-dot.offline { background-color: var(--text-dim); border: 1px solid var(--text-main); } }
.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;
+72 -50
View File
@@ -1,6 +1,6 @@
import './discordstatus.css'; import './discordstatus.css';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
interface LanyardResponse { interface LanyardResponse {
success: boolean; success: boolean;
@@ -98,19 +98,20 @@ interface DiscordActivity {
}; };
} }
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) if (applicationId && image)
return `https://cdn.discordapp.com/app-assets/${applicationId}/${image}.png`; return `https://cdn.discordapp.com/app-assets/${applicationId}/${image}.png`;
@@ -119,7 +120,7 @@ function resolveDiscordAsset(applicationId: string | undefined, image: string |
} }
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> = {
@@ -138,12 +139,13 @@ export function DiscordStatus({ userId }: DiscordStatusParams) {
async function fetchRichPresence() { async function fetchRichPresence() {
try { try {
const response = await fetch(`https://api.lanyard.rest/v1/users/${userId}`); const response = await fetch(
`https://api.lanyard.rest/v1/users/${userId}`
);
const json: LanyardResponse = await response.json(); const json: LanyardResponse = await response.json();
if (json.success) if (json.success) setPresence(json.data);
setPresence(json.data);
} catch (error) { } catch (error) {
console.error("Failed to fetch Lanyard presence:", error); console.error('Failed to fetch Lanyard presence:', error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -164,17 +166,12 @@ export function DiscordStatus({ userId }: DiscordStatusParams) {
}; };
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') if (document.visibilityState === 'visible') startPolling();
startPolling(); else stopPolling();
else
stopPolling();
}; };
if (document.visibilityState === 'visible') if (document.visibilityState === 'visible') startPolling();
startPolling(); else setLoading(false);
else
setLoading(false);
document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -185,68 +182,93 @@ export function DiscordStatus({ userId }: DiscordStatusParams) {
}, [userId]); }, [userId]);
if (loading) if (loading)
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)'
}}
>
loading status...
</p>
);
if (!presence) if (!presence)
return <p style={{ fontSize: '0.75rem', fontStyle: 'italic', color: 'var(--text-dim)' }}>offline</p>; return (
<p
style={{
fontSize: '0.75rem',
fontStyle: 'italic',
color: 'var(--text-dim)'
}}
>
offline
</p>
);
const customActivity = presence.activities.find(act => act.id === "custom"); const customActivity = presence.activities.find((act) => act.id === 'custom');
const customStatusText = customActivity const customStatusText = customActivity
? `${customActivity.emoji?.name || ''} ${customActivity.state || ''}`.trim() ? `${customActivity.emoji?.name || ''} ${customActivity.state || ''}`.trim()
: null; : null;
const gameActivity = presence.activities const gameActivity = presence.activities
.filter(act => act.type === 0) .filter((act) => act.type === 0)
.sort((a, b) => (b.assets ? 1 : 0) - (a.assets ? 1 : 0))[0] as DiscordActivity | undefined; .sort((a, b) => (b.assets ? 1 : 0) - (a.assets ? 1 : 0))[0] as
| DiscordActivity
| undefined;
const isListeningToSpotify = presence.listening_to_spotify && presence.spotify; const isListeningToSpotify =
presence.listening_to_spotify && presence.spotify;
let primaryActivity = null; let primaryActivity = null;
let activityText = ""; let activityText = '';
let activityImage = ""; let activityImage = '';
if (gameActivity) { if (gameActivity) {
primaryActivity = gameActivity; primaryActivity = gameActivity;
activityText = `playing: ${gameActivity.name.toLowerCase()}`; activityText = `playing: ${gameActivity.name.toLowerCase()}`;
if (gameActivity.details) if (gameActivity.details) activityText += `${gameActivity.details}`;
activityText += `${gameActivity.details}`;
if (gameActivity.assets) { if (gameActivity.assets) {
const targetImage = gameActivity.assets.small_image || gameActivity.assets.large_image; const targetImage =
activityImage = resolveDiscordAsset(gameActivity.application_id, targetImage); gameActivity.assets.small_image || gameActivity.assets.large_image;
activityImage = resolveDiscordAsset(
gameActivity.application_id,
targetImage
);
} }
} } else if (isListeningToSpotify && presence.spotify) {
else if (isListeningToSpotify && presence.spotify) { primaryActivity = { name: 'Spotify' } as DiscordActivity;
primaryActivity = { name: "Spotify" } as DiscordActivity;
activityText = `listening to: ${presence.spotify.song}${presence.spotify.artist}`; activityText = `listening to: ${presence.spotify.song}${presence.spotify.artist}`;
activityImage = presence.spotify.album_art_url || ""; activityImage = presence.spotify.album_art_url || '';
} }
return ( return (
<div className="discord-status-compact"> <div className='discord-status-compact'>
<div className="avatar-container"> <div className='avatar-container'>
<img <img
src={`https://api.lanyard.rest/${userId}.png`} src={`https://api.lanyard.rest/${userId}.png`}
alt="Discord avatar" alt='Discord avatar'
className="discord-avatar" className='discord-avatar'
/> />
<span className={`status-dot ${presence.discord_status}`} /> <span className={`status-dot ${presence.discord_status}`} />
</div> </div>
<div className="status-details"> <div className='status-details'>
<span className="status-text"> <span className='status-text'>
{primaryActivity ? ( {primaryActivity ? (
<>{activityText}</> <>{activityText}</>
) : ( ) : (
<>currently: <em>{STATUS_LABELS[presence.discord_status]}</em></> <>
currently: <em>{STATUS_LABELS[presence.discord_status]}</em>
</>
)} )}
</span> </span>
{customStatusText && ( {customStatusText && (
<span className="status-bubble-inline"> <span className='status-bubble-inline'>{customStatusText}</span>
{customStatusText}
</span>
)} )}
</div> </div>
@@ -254,7 +276,7 @@ export function DiscordStatus({ userId }: DiscordStatusParams) {
<img <img
src={activityImage} src={activityImage}
alt={primaryActivity.name} alt={primaryActivity.name}
className="game-icon" className='game-icon'
/> />
)} )}
</div> </div>
+43 -24
View File
@@ -2,19 +2,27 @@
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';
import { fearState } from './state'; import { FEAR_SETTINGS, fearState } from './state';
import TheCreature from './scene-components/creature'; import TheCreature from './scene-components/creature';
import Player from './scene-components/player'; import Player from './scene-components/player';
import Hallway from './scene-components/hallway'; import Hallway from './scene-components/hallway';
import { AudioListener } from 'three'; import { AudioListener } from 'three';
import FinaleText from './scene-components/finale-text';
function PostProcessing() { function PostProcessing() {
const [wasCaught, setWasCaught] = useState(fearState.wasCaught); const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
@@ -26,16 +34,15 @@ function PostProcessing() {
return () => unsubscribe(); return () => unsubscribe();
}, []); }, []);
return (<EffectComposer> return (
<Pixelation granularity={wasCaught ? 18 : 12} /> <EffectComposer>
<Pixelation granularity={wasCaught ? 18 : 10} />
<Vignette /> <Vignette />
<Noise opacity={wasCaught ? 0.01 : 0.005} /> <Noise opacity={wasCaught ? 0.01 : 0.003} />
<BrightnessContrast <BrightnessContrast brightness={-0.01} contrast={0.05} />
brightness={-0.01}
contrast={0.05}
/>
<HueSaturation saturation={wasCaught ? 1 : 0} /> <HueSaturation saturation={wasCaught ? 1 : 0} />
</EffectComposer>) </EffectComposer>
);
} }
function ListenerCreator() { function ListenerCreator() {
@@ -67,27 +74,34 @@ export default function Fear() {
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 <Canvas
shadows shadows
gl={{ antialias: true }} gl={{ antialias: true }}
className='canvas' className='canvas'
camera={{ position: [0, 3, -5], fov: 65, far: 100 }} 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} /> <ambientLight intensity={0.0225} />
)}
{FEAR_SETTINGS.TEST_MODE ? null : (
<fogExp2 attach='fog' args={[0x050505, 0.035]} /> <fogExp2 attach='fog' args={[0x050505, 0.035]} />
<PostProcessing /> )}
{FEAR_SETTINGS.TEST_MODE ? null : <PostProcessing />}
<Suspense fallback={null}> <Suspense fallback={null}>
<Hallway /> <Hallway />
@@ -96,22 +110,27 @@ export default function Fear() {
</Suspense> </Suspense>
<AmbientSound <AmbientSound
key="ambient-1" key='ambient-1'
url='fear/snd/ambience.mp3' url='fear/snd/ambience.mp3'
volume={isRustActive ? 0 : 1} 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
key='ambient-glitch'
url='fear/snd/glitch.mp3' url='fear/snd/glitch.mp3'
volume={1} volume={1}
/> : null} />
) : null}
</Canvas> </Canvas>
</>)
<FinaleText />
</>
);
} }
+37 -34
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 audio.volume = targetVolumeRef.current;
if (audio.volume > 0 && audio.paused) { if (audio.volume > 0 && audio.paused) {
audio.play().catch((err) => { audio.play().catch((err) => {
console.warn('Autoplay management holding clip playback execution.', err) console.warn(
}) 'Autoplay management holding clip playback execution.',
} err
);
});
} }
};
attemptPlay() attemptPlay();
window.addEventListener('click', attemptPlay) window.addEventListener('click', attemptPlay);
window.addEventListener('keydown', attemptPlay) window.addEventListener('keydown', attemptPlay);
return () => { return () => {
componentsMounted = false componentsMounted = false;
window.removeEventListener('click', attemptPlay) window.removeEventListener('click', attemptPlay);
window.removeEventListener('keydown', attemptPlay) window.removeEventListener('keydown', attemptPlay);
audio.pause() audio.pause();
audio.src = '' audio.src = '';
audioRef.current = null audioRef.current = null;
} };
}, [url]) }, [url]);
useEffect(() => { useEffect(() => {
const audio = audioRef.current const audio = audioRef.current;
if (!audio) return if (!audio) return;
if (volume === 0) { if (volume === 0) {
if (!audio.paused) audio.pause() if (!audio.paused) audio.pause();
} else { } else {
audio.volume = volume audio.volume = volume;
if (audio.paused) { if (audio.paused) {
audio.play().catch(() => {}) audio.play().catch(() => {});
} }
} }
}, [volume]) }, [volume]);
return null return null;
} }
+102 -19
View File
@@ -1,14 +1,22 @@
import { useTexture, PositionalAudio } from "@react-three/drei"; import { useTexture, PositionalAudio } from '@react-three/drei';
import { useFrame, useThree } from "@react-three/fiber"; import { useFrame, useThree } from '@react-three/fiber';
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from 'react';
import * as THREE from "three"; import * as THREE from 'three';
import { FEAR_SETTINGS, fearState } from "../state"; import { FEAR_SETTINGS, fearState } from '../state';
import { ShaderPatch } from '../shader-patch';
useTexture.preload('fear/img/creature.png'); useTexture.preload('fear/img/creature.png');
export default function TheCreature() { export default function TheCreature() {
const texture = useTexture('fear/img/creature.png'); const baseTexture = useTexture('fear/img/creature.png');
const texture = useMemo(() => {
const t = baseTexture.clone();
t.needsUpdate = true;
return t;
}, [baseTexture]);
const meshRef = useRef<THREE.Mesh>(null); const meshRef = useRef<THREE.Mesh>(null);
const audioRef = useRef<THREE.PositionalAudio>(null); const audioRef = useRef<THREE.PositionalAudio>(null);
const { camera } = useThree(); const { camera } = useThree();
@@ -17,10 +25,19 @@ export default function TheCreature() {
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 phaseTimer = useRef<number>(1.5);
const glitchCooldown = useRef<number>(0);
const isGlitchSpiking = useRef<boolean>(false);
const flickerCooldown = useRef<number>(0);
useEffect(() => { useEffect(() => {
const unsubscribe = fearState.subscribe(() => { const unsubscribe = fearState.subscribe(() => {
setFinaleTriggered(fearState.finaleTriggered); setFinaleTriggered(fearState.finaleTriggered);
@@ -30,6 +47,8 @@ export default function TheCreature() {
setHasTriggered(false); setHasTriggered(false);
globalDistance.current = 32; globalDistance.current = 32;
audioPlaying.current = false; audioPlaying.current = false;
movePhase.current = 'frozen';
phaseTimer.current = 1.5;
if (audioRef.current && audioRef.current.isPlaying) if (audioRef.current && audioRef.current.isPlaying)
audioRef.current.stop(); audioRef.current.stop();
@@ -47,16 +66,31 @@ export default function TheCreature() {
if (!isSpawned) { if (!isSpawned) {
setIsSpawned(true); setIsSpawned(true);
globalDistance.current = 32; globalDistance.current = 32;
movePhase.current = 'frozen';
phaseTimer.current = 1.0 + Math.random() * 1.5;
} }
if (!hasTriggered) { if (!hasTriggered) {
if (globalDistance.current < 40) { if (globalDistance.current < 40) setHasTriggered(true);
setHasTriggered(true);
}
} }
if (hasTriggered) { if (hasTriggered) {
globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * delta; phaseTimer.current -= delta;
if (phaseTimer.current <= 0) {
if (movePhase.current === 'frozen') {
movePhase.current = 'lurching';
phaseTimer.current = 0.05 + Math.random() * 0.2;
} else {
movePhase.current = 'frozen';
const proximityFactor = Math.max(0.05, globalDistance.current / 32);
phaseTimer.current = (0.2 + Math.random() * 1.0) * proximityFactor;
}
}
if (movePhase.current === 'lurching') {
globalDistance.current -= FEAR_SETTINGS.CREATURE_SPEED * 3 * delta;
}
if (audioRef.current && !audioPlaying.current) { if (audioRef.current && !audioPlaying.current) {
audioPlaying.current = true; audioPlaying.current = true;
@@ -65,7 +99,8 @@ export default function TheCreature() {
audioRef.current.play(); audioRef.current.play();
} }
const shakeIntensity = Math.max(0, 1 - (globalDistance.current / 32)) * 0.22; const shakeIntensity =
Math.max(0, 1 - globalDistance.current / 32) * 0.22;
camera.position.x += (Math.random() - 0.5) * shakeIntensity; camera.position.x += (Math.random() - 0.5) * shakeIntensity;
camera.position.y += (Math.random() - 0.5) * shakeIntensity; camera.position.y += (Math.random() - 0.5) * shakeIntensity;
@@ -80,28 +115,76 @@ export default function TheCreature() {
camera.getWorldDirection(forwardVector); camera.getWorldDirection(forwardVector);
const lookDirZ = forwardVector.z < 0 ? -1 : 1; const lookDirZ = forwardVector.z < 0 ? -1 : 1;
const calculatedZ = camera.position.z + (lookDirZ * globalDistance.current); const calculatedZ = camera.position.z + lookDirZ * globalDistance.current;
creature.position.set(0, 1.6, calculatedZ); creature.position.set(0, 1.6, calculatedZ);
creature.lookAt(camera.position.x, creature.position.y, camera.position.z); creature.lookAt(camera.position.x, creature.position.y, camera.position.z);
if (!hasTriggered) return;
const proximity = 1 - Math.max(0, Math.min(1, globalDistance.current / 32));
const jitterX = 1.0 + (Math.random() - 0.5) * 0.04 * proximity;
let jitterY = 1.0 + (Math.random() - 0.5) * 0.06 * proximity;
glitchCooldown.current -= delta;
if (glitchCooldown.current <= 0) {
if (Math.random() < 0.25 + proximity * 0.35) {
isGlitchSpiking.current = true;
glitchCooldown.current = 0.03 + Math.random() * 0.08;
} else {
isGlitchSpiking.current = false;
glitchCooldown.current =
0.08 + Math.random() * 0.4 * (1 - proximity * 0.7);
}
}
if (isGlitchSpiking.current) {
const spike = 0.15 + Math.random() * 0.35;
jitterY += Math.random() > 0.5 ? spike : -spike * 0.6;
}
creature.scale.set(jitterX, jitterY, 1.0);
flickerCooldown.current -= delta;
if (flickerCooldown.current <= 0) {
if (creature.visible && Math.random() < 0.12 + proximity * 0.08) {
creature.visible = false;
flickerCooldown.current = 0.02 + Math.random() * 0.05;
} else {
creature.visible = true;
flickerCooldown.current =
0.05 + Math.random() * 0.3 * (1 - proximity * 0.5);
}
}
texture.offset.set(
(Math.random() - 0.5) * 0.025 * proximity,
(Math.random() - 0.5) * 0.025 * proximity
);
if (proximity > 0.2) {
creature.position.x += (Math.random() - 0.5) * 0.12 * proximity;
creature.position.y += (Math.random() - 0.5) * 0.06 * proximity;
}
}); });
return ( return (
<mesh <mesh ref={meshRef} visible={finaleTriggered}>
ref={meshRef}
visible={finaleTriggered}
>
<planeGeometry args={[3.0, 4.8]} /> <planeGeometry args={[3.0, 4.8]} />
<meshBasicMaterial <meshStandardMaterial
map={texture} map={texture}
transparent={true} transparent={true}
depthWrite={false} depthWrite={false}
side={THREE.DoubleSide} side={THREE.DoubleSide}
onBeforeCompile={ShaderPatch}
emissive='#ffffff'
emissiveMap={texture}
emissiveIntensity={0.15}
/> />
{finaleTriggered && ( {finaleTriggered && (
<PositionalAudio <PositionalAudio
url="fear/snd/riser.mp3" url='fear/snd/riser.mp3'
ref={audioRef} ref={audioRef}
distance={25} distance={25}
loop={false} loop={false}
@@ -0,0 +1,82 @@
@font-face {
font-family: 'VCR';
src: url('/fear/fonts/vcr.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
.finale-container {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0vh;
display: grid;
align-items: center;
align-content: center;
justify-content: center;
overflow: hidden;
/* filter: invert(100%); */
backdrop-filter: brightness(100%);
grid-auto-rows: 5vh;
/* grid-template-columns: 0; */
grid-template-rows: repeat(auto-fit, max-content);
user-select: none;
will-change: filter;
animation: invertFlicker 0.07s infinite alternate;
}
@keyframes invertFlicker {
0%,
43%,
45%,
88%,
92% {
filter: invert(0%) contrast(100%) brightness(100%);
backdrop-filter: brightness(100%) hue-rotate(0deg);
}
44%,
46%,
89%,
93%,
100% {
filter: invert(100%) contrast(300%) brightness(150%);
backdrop-filter: brightness(30%) hue-rotate(180deg) saturate(500%);
}
}
.finale-text {
font-family: 'VCR', sans-serif;
font-variant-numeric: tabular-nums;
letter-spacing: 0.1em;
height: 0px;
width: 100%;
color: rgb(255, 255, 255);
font-size: 8vh;
text-align: center;
white-space: nowrap;
}
.scanlines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 900;
background: repeating-linear-gradient(
rgba(0, 0, 0, 0) 0px,
rgba(0, 0, 0, 0) 2px,
rgba(0, 0, 0, 0.3) 2px,
rgba(0, 0, 0, 0.3) 4px
);
}
@@ -0,0 +1,86 @@
import { JSX, useEffect, useState } from 'react';
import { fearState } from '../state';
import './finale-text.css';
const BLOCKS = [
'▀',
'▂',
'▃',
'▄',
'▅',
'▆',
'▇',
'█',
'▉',
'▊',
'▋',
'▌',
'▍',
'▎',
'▏',
'▐',
'░',
'▒',
'▓',
'▔',
'▕',
'▖',
'▗',
'▘',
'▙',
'▚',
'▛',
'▜',
'▝',
'▞',
'▟'
];
export default function FinaleText() {
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
const [elements, setElements] = useState<JSX.Element[]>([]);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWasCaught(fearState.wasCaught);
});
return () => unsubscribe();
}, []);
useEffect(() => {
if (!wasCaught) return;
const interval = setInterval(() => {
if (Math.random() > 0.9) return;
const baseText = 'bwaaaaaaaaa';
const corrupted = baseText
.split('')
.map((char) =>
Math.random() > 0.98
? BLOCKS[Math.floor(Math.random() * BLOCKS.length)]
: char
)
.join('');
setElements((prev) => [
...prev.slice(-30),
<span className='finale-text' key={crypto.randomUUID()}>
{corrupted}
</span>
]);
}, 10);
return () => clearInterval(interval);
}, [wasCaught]);
if (!wasCaught) return null;
return (
<>
<div className='finale-container'>{elements}</div>
<div className='scanlines' />
</>
);
}
+239 -73
View File
@@ -1,31 +1,80 @@
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 } 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';
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 currentSound = useRef<string | null>(null);
const steelTex = useTexture('fear/img/steel.png');
useEffect(() => {
const interval = setInterval(() => {
if (Math.random() < 0.02) {
const chosenSound =
Math.random() < 0.5 ? 'fear/snd/knock1.mp3' : 'fear/snd/knock2.mp3';
setSoundUrl(chosenSound);
currentSound.current = chosenSound;
}
}, 5000);
return () => clearInterval(interval);
}, []);
const handleAudioEnded = () => {
setSoundUrl(null);
currentSound.current = null;
};
return ( return (
<group position={position} rotation={rotation}> <group position={position} rotation={rotation}>
<mesh position={[0, 2, -0.14]}> {/* frame */}
<boxGeometry args={[2.4, 4.0, 0.2]} /> <mesh position={[0, 2, -0.1]}>
<meshStandardMaterial color="#8a8585" roughness={0.8} metalness={0.2} /> <boxGeometry args={[2.4, 4.0, 0.2, 4, 4, 1]} />
<meshStandardMaterial
map={steelTex}
color='#8d8d8d'
onBeforeCompile={ShaderPatch}
/>
</mesh> </mesh>
<mesh position={[0, 1.95, -0.08]}> {/* panel */}
<boxGeometry args={[2.1, 3.8, 0.1]} /> <mesh position={[0, 1.95, -0.0]}>
<meshStandardMaterial color="#4e4b4b" roughness={0.7} metalness={0.2} /> <boxGeometry args={[2.1, 3.8, 0.1, 4, 4, 1]} />
<meshStandardMaterial
map={steelTex}
color='#4e4a4a'
onBeforeCompile={ShaderPatch}
/>
</mesh> </mesh>
<mesh position={[0.9, 1.8, 0.08]}> {/* handle */}
<boxGeometry args={[0.08, 0.08, 0.15]} /> <mesh position={[0.75, 1.8, 0.085]}>
<meshStandardMaterial color="#4e4b4b" roughness={0.4} metalness={0.2} /> <boxGeometry args={[0.3, 0.08, 0.1]} />
<meshStandardMaterial
map={steelTex}
color='#ffffff'
onBeforeCompile={ShaderPatch}
/>
</mesh> </mesh>
{soundUrl && (
<PositionalAudio
url={soundUrl}
distance={25}
loop={false}
autoplay={true}
onEnded={handleAudioEnded}
/>
)}
</group> </group>
); );
} }
@@ -47,7 +96,6 @@ export default function Hallway() {
}); });
}, [floorTex, wallTex, rustWallTex, rustFloorTex]); }, [floorTex, wallTex, rustWallTex, rustFloorTex]);
const segmentPool = [0, 1, 2, 3, 4]; const segmentPool = [0, 1, 2, 3, 4];
const segmentCount = segmentPool.length; const segmentCount = segmentPool.length;
@@ -102,7 +150,8 @@ export default function Hallway() {
} 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 (lightState.current === 'dead') {
@@ -137,7 +186,8 @@ export default function Hallway() {
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 =
poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ;
segGroup.position.z = segmentZIndex * length; 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);
@@ -146,25 +196,26 @@ export default function Hallway() {
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;
}); });
} }
@@ -181,7 +232,8 @@ export default function Hallway() {
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')
mat.emissive.setHSL(0.07, 0.4, Math.min(intensity1, 0.7));
else mat.emissive.setHex(0xa8a1a1); else mat.emissive.setHex(0xa8a1a1);
} }
} else { } else {
@@ -196,8 +248,18 @@ export default function Hallway() {
/* /*
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[],
defaultTex: THREE.Texture,
targetRustTex: THREE.Texture,
activeColor: string,
defaultColor: string,
activeRough: number,
defaultRough: number,
activeMetal: number,
defaultMetal: number
) => {
materials.forEach((mat) => {
if (!mat) return; if (!mat) return;
const targetTex = isRustActive ? targetRustTex : defaultTex; const targetTex = isRustActive ? targetRustTex : defaultTex;
if (mat.map !== targetTex) { if (mat.map !== targetTex) {
@@ -210,19 +272,39 @@ export default function Hallway() {
}); });
}; };
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;
}); });
@@ -233,91 +315,163 @@ export default function Hallway() {
{segmentPool.map((poolIndex) => ( {segmentPool.map((poolIndex) => (
<group <group
key={poolIndex} key={poolIndex}
ref={(el) => { if (el) segmentsRef.current[poolIndex] = el; }} ref={(el) => {
if (el) segmentsRef.current[poolIndex] = el;
}}
position={[0, 0, 0]} position={[0, 0, 0]}
> >
{/* lights */} {/* lights */}
<group position={[0, FEAR_SETTINGS.HALLWAY_HEIGHT - 0.1, -FEAR_SETTINGS.HALLWAY_LENGTH / 4]}> <group
position={[
0,
FEAR_SETTINGS.HALLWAY_HEIGHT - 0.1,
-FEAR_SETTINGS.HALLWAY_LENGTH / 4
]}
>
<pointLight <pointLight
ref={(el) => { lightRefs.current[poolIndex] = el; }} ref={(el) => {
lightRefs.current[poolIndex] = el;
}}
intensity={0.9} intensity={0.9}
distance={15} distance={15}
color="#a8a1a1" color='#a8a1a1'
/> />
<mesh position={[0, 0.09, 0]}> <mesh position={[0, 0.09, 0]}>
<boxGeometry args={[0.3, 0.01, 0.3]} /> <boxGeometry args={[0.3, 0.01, 0.3]} />
<meshStandardMaterial <meshStandardMaterial
ref={(el) => { matRefs.current[poolIndex] = el; }} ref={(el) => {
color="#111111" matRefs.current[poolIndex] = el;
emissive="#a8a1a1" }}
color='#111111'
emissive='#a8a1a1'
emissiveIntensity={0.8} emissiveIntensity={0.8}
roughness={0.9} roughness={0.9}
onBeforeCompile={ShaderPatch}
/> />
</mesh> </mesh>
</group> </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]} /> <planeGeometry
args={[
FEAR_SETTINGS.HALLWAY_WIDTH,
FEAR_SETTINGS.HALLWAY_LENGTH,
4,
10
]}
/>
<meshStandardMaterial <meshStandardMaterial
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }} ref={(el) => {
if (el) floorMaterialsRef.current.push(el);
}}
map={floorTex} map={floorTex}
roughness={0.8} onBeforeCompile={ShaderPatch}
metalness={0.2}
/> />
</mesh> </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,
FEAR_SETTINGS.HALLWAY_HEIGHT,
-FEAR_SETTINGS.HALLWAY_LENGTH / 2
]}
> >
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH]} /> <planeGeometry
args={[
FEAR_SETTINGS.HALLWAY_WIDTH,
FEAR_SETTINGS.HALLWAY_LENGTH,
4,
10
]}
/>
<meshStandardMaterial <meshStandardMaterial
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }} ref={(el) => {
if (el) floorMaterialsRef.current.push(el);
}}
map={floorTex} map={floorTex}
roughness={0.8} onBeforeCompile={ShaderPatch}
metalness={0.2}
/> />
</mesh> </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]} /> rotation={[0, Math.PI / 2, 0]}
position={[
0,
FEAR_SETTINGS.HALLWAY_HEIGHT / 2,
-FEAR_SETTINGS.HALLWAY_LENGTH / 2
]}
>
<planeGeometry
args={[
FEAR_SETTINGS.HALLWAY_LENGTH,
FEAR_SETTINGS.HALLWAY_HEIGHT,
10,
4
]}
/>
<meshStandardMaterial <meshStandardMaterial
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }} ref={(el) => {
if (el) wallMaterialsRef.current.push(el);
}}
map={wallTex} map={wallTex}
roughness={0.7} onBeforeCompile={ShaderPatch}
metalness={0.1}
/> />
</mesh> </mesh>
{!isRustActive && ( {!isRustActive && (
<> <>
<Door position={[0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.25]} rotation={[0, Math.PI / 2, 0]} /> <Door
<Door position={[0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.85]} rotation={[0, Math.PI / 2, 0]} /> 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> </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]} /> rotation={[0, -Math.PI / 2, 0]}
position={[
0,
FEAR_SETTINGS.HALLWAY_HEIGHT / 2,
-FEAR_SETTINGS.HALLWAY_LENGTH / 2
]}
>
<planeGeometry
args={[
FEAR_SETTINGS.HALLWAY_LENGTH,
FEAR_SETTINGS.HALLWAY_HEIGHT,
10,
4
]}
/>
<meshStandardMaterial <meshStandardMaterial
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }} ref={(el) => {
if (el) wallMaterialsRef.current.push(el);
}}
map={wallTex} map={wallTex}
roughness={0.7} onBeforeCompile={ShaderPatch}
metalness={0.1}
/> />
</mesh> </mesh>
{!isRustActive && ( {!isRustActive && (
<Door position={[-0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.65]} rotation={[0, -Math.PI / 2, 0]} /> <Door
position={[-0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.65]}
rotation={[0, -Math.PI / 2, 0]}
/>
)} )}
</group> </group>
@@ -327,33 +481,45 @@ export default function Hallway() {
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,
FEAR_SETTINGS.HALLWAY_HEIGHT - 0.2,
-FEAR_SETTINGS.HALLWAY_LENGTH / 2
]}
> >
<cylinderGeometry args={[0.06, 0.06, FEAR_SETTINGS.HALLWAY_LENGTH, 4]} /> <cylinderGeometry
args={[0.06, 0.06, FEAR_SETTINGS.HALLWAY_LENGTH, 4]}
/>
<meshStandardMaterial <meshStandardMaterial
ref={(el) => el && pipeMaterialsRef.current.push(el)} ref={(el) => el && pipeMaterialsRef.current.push(el)}
color="#a5aca8" color='#a5aca8'
roughness={0.0} roughness={0.0}
metalness={0.4} metalness={0.4}
onBeforeCompile={ShaderPatch}
/> />
</mesh> </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,
FEAR_SETTINGS.HALLWAY_HEIGHT - 0.15,
zOffset
]}
> >
<boxGeometry args={[0.7, 0.3, 0.15]} /> <boxGeometry args={[0.7, 0.3, 0.15]} />
<meshStandardMaterial <meshStandardMaterial
ref={(el) => el && bracketMaterialsRef.current.push(el)} ref={(el) => el && bracketMaterialsRef.current.push(el)}
color="#a5aca8" color='#a5aca8'
roughness={0.0} roughness={0.0}
metalness={0.4} metalness={0.4}
onBeforeCompile={ShaderPatch}
/> />
</mesh> </mesh>
); );
+91 -31
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,21 +14,33 @@ 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 === 'KeyS' || e.code === 'ArrowDown')
keys.current.Backward = true;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = true; if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = true;
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = 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);
@@ -54,10 +66,39 @@ export default function Player() {
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 hasStepped = useRef<boolean>(false);
useEffect(() => { useEffect(() => {
playerRoot.set(camera.position.x, FEAR_SETTINGS.PLAYER_HEIGHT, camera.position.z); 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;
const randomIndex = Math.floor(
Math.random() * footstepAudio.current.length
);
const audio = footstepAudio.current[randomIndex];
audio.currentTime = 0;
audio.play().catch((err) => {
console.warn(
'Footstep playback blocked by browser autocomplete/interaction rules.',
err
);
});
};
useFrame((state, delta) => { useFrame((state, delta) => {
const dt = Math.min(delta, 0.1); const dt = Math.min(delta, 0.1);
@@ -85,43 +126,59 @@ export default function Player() {
const maxX = 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 = THREE.MathUtils.clamp(playerRoot.x, minX, maxX);
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right; const isMoving =
controls.Forward || controls.Backward || controls.Left || controls.Right;
bobIntensity.current = THREE.MathUtils.lerp(bobIntensity.current, isMoving ? 1 : 0, 8 * dt); bobIntensity.current = THREE.MathUtils.lerp(
bobIntensity.current,
isMoving ? 1 : 0,
8 * dt
);
if (isMoving) if (isMoving) movementCounter.current += dt * 12;
movementCounter.current += dt * 12;
const moveBobY = Math.sin(movementCounter.current) * 0.06 * bobIntensity.current; const sinWave = Math.sin(movementCounter.current);
const moveBobX = Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current; const moveBobY = sinWave * 0.06 * bobIntensity.current;
const moveBobX =
Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current;
if (isMoving && sinWave < -0.9) {
if (!hasStepped.current) {
playRandomFootstep();
hasStepped.current = true;
}
} else if (sinWave > 0) {
hasStepped.current = false;
}
const breatheTime = state.clock.elapsedTime * 1.8; const breatheTime = state.clock.elapsedTime * 1.8;
const breatheBobY = Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5); const breatheBobY =
Math.sin(breatheTime) * 0.03 * (1 - bobIntensity.current * 0.5);
camera.position.copy(playerRoot); camera.position.copy(playerRoot);
camera.position.y += moveBobY + breatheBobY; camera.position.y += moveBobY + breatheBobY;
camera.position.addScaledVector(side, moveBobX); camera.position.addScaledVector(side, moveBobX);
if (flashlightRef.current) { if (flashlightRef.current) {
flashlightRef.current.position.copy(camera.position); flashlightRef.current.position.lerp(camera.position, 7 * dt);
camera.getWorldDirection(viewDirection); camera.getWorldDirection(viewDirection);
targetDest targetDest.copy(camera.position).addScaledVector(viewDirection, 10);
.copy(camera.position)
.addScaledVector(viewDirection, 10);
flashlightRef.current.target.position.lerp(targetDest, 12 * dt); flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
flashlightRef.current.target.updateMatrixWorld(); flashlightRef.current.target.updateMatrixWorld();
flashlightRef.current.intensity = flashlightRef.current.intensity =
FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE + FEAR_SETTINGS.FLASHLIGHT_INTENSITY_BASE +
Math.sin(state.clock.elapsedTime * 30) * 0.15 * Math.cos(state.clock.elapsedTime * 3); Math.sin(state.clock.elapsedTime * 30) *
0.15 *
Math.cos(state.clock.elapsedTime * 3);
} }
const length = FEAR_SETTINGS.HALLWAY_LENGTH; const length = FEAR_SETTINGS.HALLWAY_LENGTH;
const absoluteZ = -playerRoot.z; const absoluteZ = -playerRoot.z;
const rawSegmentIndex = Math.floor(absoluteZ / length); const rawSegmentIndex = Math.floor(absoluteZ / length);
const progressZ = ((absoluteZ % length) + length) % length / length; const progressZ = (((absoluteZ % length) + length) % length) / length;
if (rawSegmentIndex > confirmedSegment.current && progressZ > 0.25) { if (rawSegmentIndex > confirmedSegment.current && progressZ > 0.25) {
if (!hasTriggeredThisSegment.current) { if (!hasTriggeredThisSegment.current) {
@@ -129,8 +186,7 @@ export default function Player() {
hasTriggeredThisSegment.current = true; hasTriggeredThisSegment.current = true;
} }
confirmedSegment.current = rawSegmentIndex; confirmedSegment.current = rawSegmentIndex;
} } else if (rawSegmentIndex < confirmedSegment.current && progressZ < 0.75) {
else if (rawSegmentIndex < confirmedSegment.current && progressZ < 0.75) {
if (!hasTriggeredThisSegment.current) { if (!hasTriggeredThisSegment.current) {
fearState.registerLoop('backward'); fearState.registerLoop('backward');
hasTriggeredThisSegment.current = true; hasTriggeredThisSegment.current = true;
@@ -138,7 +194,11 @@ export default function Player() {
confirmedSegment.current = rawSegmentIndex; confirmedSegment.current = rawSegmentIndex;
} }
if (rawSegmentIndex === confirmedSegment.current && progressZ > 0.35 && progressZ < 0.65) { if (
rawSegmentIndex === confirmedSegment.current &&
progressZ > 0.35 &&
progressZ < 0.65
) {
hasTriggeredThisSegment.current = false; hasTriggeredThisSegment.current = false;
} }
}); });
@@ -152,7 +212,7 @@ export default function Player() {
angle={0.35} angle={0.35}
penumbra={0.8} penumbra={0.8}
intensity={0} intensity={0}
color="#fffaed" color='#fffaed'
decay={2} decay={2}
castShadow castShadow
shadow-bias={-0.001} shadow-bias={-0.001}
+61
View File
@@ -0,0 +1,61 @@
export function ShaderPatch(shader: {
vertexShader: string;
fragmentShader: string;
uniforms: Object;
}) {
shader.vertexShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
#endif
${shader.vertexShader}
`;
shader.vertexShader = shader.vertexShader.replace(
`#include <project_vertex>`,
`
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
float precisionModifier = 200.0;
gl_Position.xy /= gl_Position.w;
gl_Position.xy = floor(gl_Position.xy * precisionModifier) / precisionModifier;
gl_Position.xy *= gl_Position.w;
vDepth = gl_Position.w;
#ifdef USE_MAP
vAffineUv = vMapUv * gl_Position.w;
#endif
`
);
shader.fragmentShader = `
varying float vDepth;
#ifdef USE_MAP
varying vec2 vAffineUv;
#endif
${shader.fragmentShader}
`;
shader.fragmentShader = shader.fragmentShader.replace(
`#include <map_fragment>`,
`
#ifdef USE_MAP
vec2 flatAffineUV = vAffineUv / max(vDepth, 0.001);
vec2 warpDiff = flatAffineUV - vMapUv;
float warpDist = length(warpDiff);
float maxDistortion = 0.25;
float falloff = maxDistortion / (maxDistortion + warpDist);
vec2 finalUV = vMapUv + (warpDiff * falloff);
vec4 texelColor = texture2D( map, finalUV );
diffuseColor *= texelColor;
#endif
`
);
}
+33 -6
View File
@@ -12,7 +12,11 @@ export const FEAR_SETTINGS = {
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,
TEST_MODE: false
}; };
const listeners = new Set<() => void>(); const listeners = new Set<() => void>();
@@ -23,10 +27,13 @@ export const fearState = {
isRustActive: false, isRustActive: false,
finaleTriggered: false, finaleTriggered: false,
wasCaught: false, wasCaught: false,
finaleProgression: 0,
subscribe(listener: () => void) { subscribe(listener: () => void) {
listeners.add(listener); listeners.add(listener);
return () => { listeners.delete(listener); }; return () => {
listeners.delete(listener);
};
}, },
emit() { emit() {
@@ -34,20 +41,40 @@ export const fearState = {
}, },
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;
this.emit();
} }
if (this.wasCaught) {
if (this.finaleProgression < FEAR_SETTINGS.EVENT_FINALE_DURATION) {
this.finaleProgression = Math.min(
this.finaleProgression + delta,
FEAR_SETTINGS.EVENT_FINALE_DURATION
);
} else {
window.location.href = '/';
}
}
this.emit();
}, },
registerLoop(direction: 'forward' | 'backward') { registerLoop(direction: 'forward' | 'backward') {
this.loopCount += 1; this.loopCount += 1;
this.isRustActive = this.loopCount >= FEAR_SETTINGS.EVENT_RUST_LOOP_COUNT; this.isRustActive = this.loopCount >= FEAR_SETTINGS.EVENT_RUST_LOOP_COUNT;
this.finaleTriggered = this.loopCount >= FEAR_SETTINGS.EVENT_FINALE_LOOP_COUNT; this.finaleTriggered =
this.loopCount >= FEAR_SETTINGS.EVENT_FINALE_LOOP_COUNT;
this.emit(); this.emit();
}, },
+20 -7
View File
@@ -2,7 +2,7 @@ import type { Metadata, Viewport } from 'next';
import './globals.css'; import './globals.css';
export const metadata: Metadata = { export const metadata: Metadata = {
title: '', title: 'site under construction',
// description: '', // description: '',
openGraph: { openGraph: {
// title: '⛧', // title: '⛧',
@@ -11,15 +11,15 @@ export const metadata: Metadata = {
{ {
url: 'https://neru.rip/img/ok.jpg', url: 'https://neru.rip/img/ok.jpg',
width: 734, width: 734,
height: 1104, height: 1104
}, }
], ]
} }
}; };
export const viewport: Viewport = { export const viewport: Viewport = {
themeColor: '#fbcfe8', themeColor: '#fbcfe8'
} };
export default function RootLayout({ export default function RootLayout({
children children
@@ -47,7 +47,20 @@ export default function RootLayout({
/> />
<link rel='manifest' href='/site.webmanifest' /> <link rel='manifest' href='/site.webmanifest' />
<html lang='en'> <html lang='en'>
<body className={`antialiased`}>{children}</body> <body className={`antialiased`}>
<div
style={{
display: 'flex',
justifyContent: 'center',
padding: '2rem'
}}
>
<img
src={'construction.jpg'}
style={{ width: '100%', maxWidth: '600px', height: 'auto' }}
/>
</div>
</body>
</html> </html>
</> </>
); );
+38 -20
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';
@@ -62,29 +73,33 @@ function Scene() {
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.65, 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: 4, step: 1 }, grassBlades: { value: 3, min: 1, max: 5, step: 1 },
grassSegments: { value: 3, min: 1, max: 5, step: 1 }, grassSegments: { value: 4, min: 1, max: 5, step: 1 },
grassLODStart: { value: 0.15, min: 0.0, max: 0.9, step: 0.05 }, 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 <Environment
files={'niko/hdr/sky.hdr'} files={'niko/hdr/sky.hdr'}
environmentIntensity={1} environmentIntensity={0.85}
background background
/> />
<fogExp2 attach='fog' args={[0x9a9a9a, 0.01]} /> <fogExp2 attach='fog' args={[0xa3a5ba, 0.0125]} />
<ambientLight intensity={0.5} />
<directionalLight position={[15, 25, 15]} intensity={1} />
<Terrain <Terrain
chunks={chunks} chunks={chunks}
@@ -109,7 +124,8 @@ function Scene() {
/> />
<SealCube /> <SealCube />
</>) </>
);
} }
function LutEffect() { function LutEffect() {
@@ -118,20 +134,22 @@ function LutEffect() {
} }
function PostProcessing() { function PostProcessing() {
return (<EffectComposer> return (
<EffectComposer>
<DepthOfField target={[0, 3, 0]} focalLength={10} bokehScale={5} /> <DepthOfField target={[0, 3, 0]} focalLength={10} bokehScale={5} />
<Vignette /> <Vignette />
<Noise opacity={0.05} /> <Noise opacity={0.05} />
<Bloom <Bloom
intensity={2} intensity={0.8}
luminanceThreshold={0.5} luminanceThreshold={0.4}
luminanceSmoothing={0.1} luminanceSmoothing={0.5}
/> />
<SMAA /> <SMAA />
<HueSaturation saturation={0.3} /> <HueSaturation saturation={0.3} />
<BrightnessContrast brightness={0.05} contrast={-0.1} /> <BrightnessContrast brightness={0.05} contrast={-0.1} />
<LutEffect /> <LutEffect />
</EffectComposer>) </EffectComposer>
);
} }
export default function Seal() { export default function Seal() {
@@ -145,11 +163,11 @@ export default function Seal() {
<Canvas <Canvas
shadows shadows
camera={{ position: [0, 5, 15], fov: 50, far: 100 }} camera={{ position: [0, 5, 15], fov: 50, far: 100 }}
gl={{ antialias: true }} 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 />
+23 -23
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;
} }
+35 -23
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';
@@ -173,24 +182,28 @@ export default function Grass({
dummy.rotation.x = (Math.random() - 0.5) * 0.2; dummy.rotation.x = (Math.random() - 0.5) * 0.2;
dummy.rotation.z = (Math.random() - 0.5) * 0.2; dummy.rotation.z = (Math.random() - 0.5) * 0.2;
const baseScale = grassSize + Math.random() * grassSize * 0.5; const noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
const heightMult = 0.5 + Math.random() * 1.0; const t = (noiseVal + 1) / 2;
dummy.scale.set(baseScale, baseScale * heightMult, baseScale); const randomInternal = (Math.random() - 0.5) * 0.2;
const finalT = Math.max(0, Math.min(1, t + randomInternal));
color.lerpColors(dryColorObj, lushColorObj, finalT);
meshRef.current.setColorAt(instanceIndex, color);
const heightNoise = noise2D(globalX * 0.08, globalZ * 0.08);
const macroHeight = (heightNoise + 1.0) * 0.5; // 0..1
const microNoise = noise2D(globalX * 0.3, globalZ * 0.3);
const microHeight = (microNoise + 1.0) * 0.25; // 0..0.5
const perBladeRandom = Math.random() * 0.4;
const grassWidth = grassSize * (0.7 + Math.random() * 0.5);
const grassHeight =
grassSize * (0.4 + macroHeight * 0.8 + microHeight + perBladeRandom);
dummy.scale.set(grassWidth, grassHeight, grassWidth);
dummy.updateMatrix(); dummy.updateMatrix();
meshRef.current.setMatrixAt(instanceIndex, dummy.matrix); meshRef.current.setMatrixAt(instanceIndex, dummy.matrix);
const noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
const t = (noiseVal + 1) / 2;
const randomInternal = (Math.random() - 0.5) * 0.2;
const finalT = Math.max(0, Math.min(1, t + randomInternal));
color.lerpColors(dryColorObj, lushColorObj, finalT);
meshRef.current.setColorAt(instanceIndex, color);
instanceIndex++; instanceIndex++;
} }
meshRef.current.count = instanceIndex; meshRef.current.count = instanceIndex;
@@ -224,6 +237,7 @@ export default function Grass({
shader.vertexShader = ` shader.vertexShader = `
uniform float uTime; uniform float uTime;
varying vec2 vGrassUv; varying vec2 vGrassUv;
varying vec3 vWorldPos;
float hash(vec2 p) { float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
@@ -233,12 +247,10 @@ export default function Grass({
vec2 i = floor(p); vec2 i = floor(p);
vec2 f = fract(p); vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f); f = f * f * (3.0 - 2.0 * f);
float a = hash(i); float a = hash(i);
float b = hash(i + vec2(1.0, 0.0)); float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0)); float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0)); float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
} }
@@ -246,23 +258,21 @@ export default function Grass({
float value = 0.0; float value = 0.0;
float amplitude = 0.5; float amplitude = 0.5;
float frequency = 1.0; float frequency = 1.0;
for(int i = 0; i < 4; i++) { for(int i = 0; i < 4; i++) {
value += amplitude * noise(p * frequency); value += amplitude * noise(p * frequency);
frequency *= 2.0; frequency *= 2.0;
amplitude *= 0.5; amplitude *= 0.5;
} }
return value; return value;
} }
${shader.vertexShader} ${shader.vertexShader}
`; `;
shader.vertexShader = shader.vertexShader.replace( shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>', '#include <begin_vertex>',
` `
#include <begin_vertex> #include <begin_vertex>
vGrassUv = uv;
${grassVert} ${grassVert}
` `
); );
@@ -270,8 +280,10 @@ export default function Grass({
shader.fragmentShader = ` shader.fragmentShader = `
uniform float uTime; uniform float uTime;
varying vec2 vGrassUv; varying vec2 vGrassUv;
varying vec3 vWorldPos;
${shader.fragmentShader} ${shader.fragmentShader}
`; `;
shader.fragmentShader = shader.fragmentShader.replace( shader.fragmentShader = shader.fragmentShader.replace(
'#include <color_fragment>', '#include <color_fragment>',
` `
+11 -4
View File
@@ -1,6 +1,6 @@
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');
@@ -12,13 +12,20 @@ const SealCube = forwardRef<Mesh>((props, ref) => {
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;
} }
}); });
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]} />
<meshBasicMaterial map={texture} depthWrite={true} /> <meshStandardMaterial
map={texture}
roughness={0.4}
metalness={0.1}
envMapIntensity={1.2}
/>
</mesh> </mesh>
); );
}); });
@@ -1,13 +1,31 @@
float ao = smoothstep(0.0, 0.7, vGrassUv.y); float ao = smoothstep(0.0, 0.7, vGrassUv.y);
ao = mix(0.05, 1.0, ao); ao = mix(0.05, 1.0, pow(ao, 1.6));
vec3 rootColor = diffuseColor.rgb * 0.4; vec3 rootColor = diffuseColor.rgb * 0.15;
vec3 tipColor = diffuseColor.rgb * 1.5; vec3 midColor = diffuseColor.rgb;
vec3 tipColor = diffuseColor.rgb * 1.3 + vec3(0.06, 0.08, 0.0);
vec3 grassColor = mix(rootColor, tipColor, vGrassUv.y); float heightParam = vGrassUv.y;
grassColor *= ao; vec3 grassColor;
if (heightParam < 0.4) {
float t = smoothstep(0.0, 0.4, heightParam);
grassColor = mix(rootColor, midColor, t);
} else {
float t = smoothstep(0.4, 1.0, heightParam);
grassColor = mix(midColor, tipColor, t);
}
float translucency = pow(vGrassUv.y, 2.0) * 0.5; vec3 viewDir = normalize(cameraPosition - vWorldPos);
grassColor += diffuseColor.rgb * translucency; vec3 lightDir = normalize(vec3(15.0, 25.0, 15.0));
diffuseColor.rgb = grassColor; float VdotL = max(0.0, dot(viewDir, -lightDir));
float sss = pow(VdotL, 3.0) * smoothstep(0.2, 0.9, vGrassUv.y);
vec3 sssColor = diffuseColor.rgb * vec3(0.6, 1.0, 0.15) * 1.8;
grassColor += sssColor * sss * 2.0;
float NdotV = 1.0 - max(0.0, dot(normalize(vNormal), viewDir));
float rim = pow(NdotV, 3.0) * smoothstep(0.3, 1.0, vGrassUv.y) * 0.15;
grassColor += vec3(0.3, 0.5, 0.1) * rim;
diffuseColor.rgb = grassColor * ao;
@@ -1,3 +1,5 @@
vGrassUv = uv;
vec4 worldPos = modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0); vec4 worldPos = modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
float gx = worldPos.x; float gx = worldPos.x;
float gz = worldPos.z; float gz = worldPos.z;
@@ -8,7 +10,6 @@ float windSpeed = 1.5;
float windTime = uTime * windSpeed; float windTime = uTime * windSpeed;
vec2 windSamplePos = (worldPos.xz * 0.05) - (mainWindDir * windTime * 0.2); vec2 windSamplePos = (worldPos.xz * 0.05) - (mainWindDir * windTime * 0.2);
float windBase = fbm(windSamplePos * 0.8) * 0.4 + 0.2; float windBase = fbm(windSamplePos * 0.8) * 0.4 + 0.2;
float gustNoise = fbm(windSamplePos * 0.4); float gustNoise = fbm(windSamplePos * 0.4);
float gust = pow(gustNoise, 3.0) * 1.8; float gust = pow(gustNoise, 3.0) * 1.8;
@@ -20,19 +21,32 @@ float spring = sin(uTime * 2.0 + phase) * 0.06 + sin(uTime * 4.5 + phase * 1.5)
float angleNoise = fbm(windSamplePos * 2.0 + uTime * 0.1) - 0.5; float angleNoise = fbm(windSamplePos * 2.0 + uTime * 0.1) - 0.5;
vec2 windDir = normalize(mainWindDir + vec2(-mainWindDir.y, mainWindDir.x) * angleNoise * 0.4); vec2 windDir = normalize(mainWindDir + vec2(-mainWindDir.y, mainWindDir.x) * angleNoise * 0.4);
float taperFactor = pow(uv.y, 4.0); // taper (fade)
float taper = 1.0 - taperFactor * 0.6; float taperFactor = uv.y * uv.y * uv.y;
float taper = 1.0 - taperFactor * 0.85;
transformed.x *= taper; transformed.x *= taper;
transformed.z *= taper; transformed.z *= taper;
// curve
float curveVal = fbm(vec2(gx, gz) * 0.5); float curveVal = fbm(vec2(gx, gz) * 0.5);
float curveStrength = 2.0 + curveVal * 2.0; float curveStrength = 1.5 + curveVal * 2.5;
float curveAmount = uv.y * uv.y * curveStrength; float curveAmount = uv.y * uv.y * curveStrength;
vec2 curveDir = normalize(vec2(curveVal, fbm(vec2(gz, gx))) - 0.5); vec2 curveDir = normalize(vec2(curveVal, fbm(vec2(gz, gx))) - 0.5);
transformed.x += curveAmount * curveDir.x * 0.5; transformed.x += curveAmount * curveDir.x * 0.4;
transformed.z += curveAmount * curveDir.y * 0.5; transformed.z += curveAmount * curveDir.y * 0.4;
// sway
float swayAmount = (totalWind + spring) * uv.y * uv.y; float swayAmount = (totalWind + spring) * uv.y * uv.y;
transformed.x += swayAmount * windDir.x; transformed.x += swayAmount * windDir.x;
transformed.z += swayAmount * windDir.y; transformed.z += swayAmount * windDir.y;
transformed.y -= abs(swayAmount) * 0.2; transformed.y -= abs(swayAmount) * 0.2;
// normal comp
vec2 totalBend = curveDir * curveAmount * 0.4 + windDir * swayAmount;
float bendMag = length(totalBend);
vec3 bentNormal = normalize(vec3(-totalBend.x * 0.5, 1.0, -totalBend.y * 0.5));
// normal mix
objectNormal = normalize(mix(vec3(0.0, 1.0, 0.0), bentNormal, uv.y));
vWorldPos = (modelMatrix * instanceMatrix * vec4(transformed, 1.0)).xyz;
+9 -7
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;
+27 -10
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 {
+120 -46
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 />
)
} }