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
+16 -4
View File
@@ -31,10 +31,22 @@
border-radius: 50%;
box-shadow: 0 0 0 2px #fffdfd;
}
.status-dot.online { background-color: #a7f3d0; border: 1px solid #34d399; }
.status-dot.idle { background-color: #fef08a; border: 1px solid #facc15; }
.status-dot.dnd { background-color: #fecdd3; border: 1px solid #fb7185; }
.status-dot.offline { background-color: var(--text-dim); border: 1px solid var(--text-main); }
.status-dot.online {
background-color: #a7f3d0;
border: 1px solid #34d399;
}
.status-dot.idle {
background-color: #fef08a;
border: 1px solid #facc15;
}
.status-dot.dnd {
background-color: #fecdd3;
border: 1px solid #fb7185;
}
.status-dot.offline {
background-color: var(--text-dim);
border: 1px solid var(--text-main);
}
.status-details {
display: flex;
+72 -50
View File
@@ -1,6 +1,6 @@
import './discordstatus.css';
import { useEffect, useState } from "react";
import { useEffect, useState } from 'react';
interface LanyardResponse {
success: boolean;
@@ -98,19 +98,20 @@ interface DiscordActivity {
};
}
function resolveDiscordAsset(applicationId: string | undefined, image: string | undefined): string {
if (!image) return "";
function resolveDiscordAsset(
applicationId: string | undefined,
image: string | undefined
): string {
if (!image) return '';
if (image.startsWith("mp:external/")) {
const httpsIndex = image.indexOf("/https/");
if (image.startsWith('mp:external/')) {
const httpsIndex = image.indexOf('/https/');
if (httpsIndex !== -1) {
return `https://${image.slice(httpsIndex + "/https/".length)}`;
return `https://${image.slice(httpsIndex + '/https/'.length)}`;
}
}
if (image.startsWith("spotify:"))
return "";
if (image.startsWith('spotify:')) return '';
if (applicationId && image)
return `https://cdn.discordapp.com/app-assets/${applicationId}/${image}.png`;
@@ -119,7 +120,7 @@ function resolveDiscordAsset(applicationId: string | undefined, image: string |
}
export interface DiscordStatusParams {
userId: string
userId: string;
}
const STATUS_LABELS: Record<LanyardData['discord_status'], string> = {
@@ -138,12 +139,13 @@ export function DiscordStatus({ userId }: DiscordStatusParams) {
async function fetchRichPresence() {
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();
if (json.success)
setPresence(json.data);
if (json.success) setPresence(json.data);
} catch (error) {
console.error("Failed to fetch Lanyard presence:", error);
console.error('Failed to fetch Lanyard presence:', error);
} finally {
setLoading(false);
}
@@ -164,17 +166,12 @@ export function DiscordStatus({ userId }: DiscordStatusParams) {
};
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible')
startPolling();
else
stopPolling();
if (document.visibilityState === 'visible') startPolling();
else stopPolling();
};
if (document.visibilityState === 'visible')
startPolling();
else
setLoading(false);
if (document.visibilityState === 'visible') startPolling();
else setLoading(false);
document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -185,68 +182,93 @@ export function DiscordStatus({ userId }: DiscordStatusParams) {
}, [userId]);
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)
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
? `${customActivity.emoji?.name || ''} ${customActivity.state || ''}`.trim()
: null;
const gameActivity = presence.activities
.filter(act => act.type === 0)
.sort((a, b) => (b.assets ? 1 : 0) - (a.assets ? 1 : 0))[0] as DiscordActivity | undefined;
.filter((act) => act.type === 0)
.sort((a, b) => (b.assets ? 1 : 0) - (a.assets ? 1 : 0))[0] as
| DiscordActivity
| undefined;
const isListeningToSpotify = presence.listening_to_spotify && presence.spotify;
const isListeningToSpotify =
presence.listening_to_spotify && presence.spotify;
let primaryActivity = null;
let activityText = "";
let activityImage = "";
let activityText = '';
let activityImage = '';
if (gameActivity) {
primaryActivity = gameActivity;
activityText = `playing: ${gameActivity.name.toLowerCase()}`;
if (gameActivity.details)
activityText += `${gameActivity.details}`;
if (gameActivity.details) activityText += `${gameActivity.details}`;
if (gameActivity.assets) {
const targetImage = gameActivity.assets.small_image || gameActivity.assets.large_image;
activityImage = resolveDiscordAsset(gameActivity.application_id, targetImage);
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;
} 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 || "";
activityImage = presence.spotify.album_art_url || '';
}
return (
<div className="discord-status-compact">
<div className="avatar-container">
<div className='discord-status-compact'>
<div className='avatar-container'>
<img
src={`https://api.lanyard.rest/${userId}.png`}
alt="Discord avatar"
className="discord-avatar"
alt='Discord avatar'
className='discord-avatar'
/>
<span className={`status-dot ${presence.discord_status}`} />
</div>
<div className="status-details">
<span className="status-text">
<div className='status-details'>
<span className='status-text'>
{primaryActivity ? (
<>{activityText}</>
) : (
<>currently: <em>{STATUS_LABELS[presence.discord_status]}</em></>
<>
currently: <em>{STATUS_LABELS[presence.discord_status]}</em>
</>
)}
</span>
{customStatusText && (
<span className="status-bubble-inline">
{customStatusText}
</span>
<span className='status-bubble-inline'>{customStatusText}</span>
)}
</div>
@@ -254,7 +276,7 @@ export function DiscordStatus({ userId }: DiscordStatusParams) {
<img
src={activityImage}
alt={primaryActivity.name}
className="game-icon"
className='game-icon'
/>
)}
</div>
+36 -20
View File
@@ -2,9 +2,16 @@
import './page.css';
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { BrightnessContrast, EffectComposer, HueSaturation, Noise, Pixelation, Vignette } from "@react-three/postprocessing";
import { Suspense, useEffect, useState } from "react";
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import {
BrightnessContrast,
EffectComposer,
HueSaturation,
Noise,
Pixelation,
Vignette
} from '@react-three/postprocessing';
import { Suspense, useEffect, useState } from 'react';
import { AmbientSound } from './scene-components/ambient-sound';
@@ -27,16 +34,15 @@ function PostProcessing() {
return () => unsubscribe();
}, []);
return (<EffectComposer>
return (
<EffectComposer>
<Pixelation granularity={wasCaught ? 18 : 10} />
<Vignette />
<Noise opacity={wasCaught ? 0.01 : 0.003} />
<BrightnessContrast
brightness={-0.01}
contrast={0.05}
/>
<BrightnessContrast brightness={-0.01} contrast={0.05} />
<HueSaturation saturation={wasCaught ? 1 : 0} />
</EffectComposer>)
</EffectComposer>
);
}
function ListenerCreator() {
@@ -68,12 +74,13 @@ export default function Fear() {
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setIsRustActive(fearState.isRustActive);
setWasCaught(fearState.wasCaught)
setWasCaught(fearState.wasCaught);
});
return () => unsubscribe();
}, []);
return (<>
return (
<>
<Canvas
shadows
gl={{ antialias: true }}
@@ -84,10 +91,16 @@ export default function Fear() {
<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 ? null : <fogExp2 attach='fog' args={[0x050505, 0.035]} />}
{FEAR_SETTINGS.TEST_MODE ? (
<ambientLight intensity={2} />
) : (
<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}>
@@ -97,24 +110,27 @@ export default function Fear() {
</Suspense>
<AmbientSound
key="ambient-1"
key='ambient-1'
url='fear/snd/ambience.mp3'
volume={isRustActive ? 0 : 0.5}
/>
<AmbientSound
key="ambient-2"
key='ambient-2'
url='fear/snd/ambience2.mp3'
volume={isRustActive ? 1 : 0}
/>
{wasCaught ? <AmbientSound
key="ambient-glitch"
{wasCaught ? (
<AmbientSound
key='ambient-glitch'
url='fear/snd/glitch.mp3'
volume={1}
/> : null}
/>
) : null}
</Canvas>
<FinaleText />
</>)
</>
);
}
+37 -34
View File
@@ -1,64 +1,67 @@
import { useEffect, useRef } from 'react'
import { useEffect, useRef } from 'react';
interface AmbientSoundProps {
url: string
volume?: number
url: string;
volume?: number;
}
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
const audioRef = useRef<HTMLAudioElement | null>(null)
const targetVolumeRef = useRef<number>(volume)
const audioRef = useRef<HTMLAudioElement | null>(null);
const targetVolumeRef = useRef<number>(volume);
targetVolumeRef.current = volume
targetVolumeRef.current = volume;
useEffect(() => {
const audio = new Audio(url)
audio.loop = true
audio.volume = 0
audioRef.current = audio
const audio = new Audio(url);
audio.loop = true;
audio.volume = 0;
audioRef.current = audio;
let componentsMounted = true
let componentsMounted = true;
const attemptPlay = () => {
if (!audioRef.current || !componentsMounted) return
if (!audioRef.current || !componentsMounted) return;
audio.volume = targetVolumeRef.current
audio.volume = targetVolumeRef.current;
if (audio.volume > 0 && audio.paused) {
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('keydown', attemptPlay)
window.addEventListener('click', attemptPlay);
window.addEventListener('keydown', attemptPlay);
return () => {
componentsMounted = false
window.removeEventListener('click', attemptPlay)
window.removeEventListener('keydown', attemptPlay)
audio.pause()
audio.src = ''
audioRef.current = null
}
}, [url])
componentsMounted = false;
window.removeEventListener('click', attemptPlay);
window.removeEventListener('keydown', attemptPlay);
audio.pause();
audio.src = '';
audioRef.current = null;
};
}, [url]);
useEffect(() => {
const audio = audioRef.current
if (!audio) return
const audio = audioRef.current;
if (!audio) return;
if (volume === 0) {
if (!audio.paused) audio.pause()
if (!audio.paused) audio.pause();
} else {
audio.volume = volume
audio.volume = volume;
if (audio.paused) {
audio.play().catch(() => {})
audio.play().catch(() => {});
}
}
}, [volume])
}, [volume]);
return null
return null;
}
+20 -20
View File
@@ -1,10 +1,10 @@
import { useTexture, PositionalAudio } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import { useEffect, useMemo, useRef, useState } from "react";
import { useTexture, PositionalAudio } from '@react-three/drei';
import { useFrame, useThree } from '@react-three/fiber';
import { useEffect, useMemo, useRef, useState } from 'react';
import * as THREE from "three";
import { FEAR_SETTINGS, fearState } from "../state";
import { ShaderPatch } from "../shader-patch";
import * as THREE from 'three';
import { FEAR_SETTINGS, fearState } from '../state';
import { ShaderPatch } from '../shader-patch';
useTexture.preload('fear/img/creature.png');
@@ -25,7 +25,9 @@ export default function TheCreature() {
const [isSpawned, setIsSpawned] = useState(false);
const globalDistance = useRef<number>(32);
const [finaleTriggered, setFinaleTriggered] = useState(fearState.finaleTriggered);
const [finaleTriggered, setFinaleTriggered] = useState(
fearState.finaleTriggered
);
const audioPlaying = useRef<boolean>(false);
@@ -69,9 +71,7 @@ export default function TheCreature() {
}
if (!hasTriggered) {
if (globalDistance.current < 40)
setHasTriggered(true);
if (globalDistance.current < 40) setHasTriggered(true);
}
if (hasTriggered) {
@@ -99,7 +99,8 @@ export default function TheCreature() {
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.y += (Math.random() - 0.5) * shakeIntensity;
@@ -114,7 +115,7 @@ export default function TheCreature() {
camera.getWorldDirection(forwardVector);
const lookDirZ = forwardVector.z < 0 ? -1 : 1;
const calculatedZ = camera.position.z + (lookDirZ * globalDistance.current);
const calculatedZ = camera.position.z + lookDirZ * globalDistance.current;
creature.position.set(0, 1.6, calculatedZ);
creature.lookAt(camera.position.x, creature.position.y, camera.position.z);
@@ -132,7 +133,8 @@ export default function TheCreature() {
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 =
0.08 + Math.random() * 0.4 * (1 - proximity * 0.7);
}
}
@@ -150,7 +152,8 @@ export default function TheCreature() {
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 =
0.05 + Math.random() * 0.3 * (1 - proximity * 0.5);
}
}
@@ -166,10 +169,7 @@ export default function TheCreature() {
});
return (
<mesh
ref={meshRef}
visible={finaleTriggered}
>
<mesh ref={meshRef} visible={finaleTriggered}>
<planeGeometry args={[3.0, 4.8]} />
<meshStandardMaterial
map={texture}
@@ -177,14 +177,14 @@ export default function TheCreature() {
depthWrite={false}
side={THREE.DoubleSide}
onBeforeCompile={ShaderPatch}
emissive="#ffffff"
emissive='#ffffff'
emissiveMap={texture}
emissiveIntensity={0.15}
/>
{finaleTriggered && (
<PositionalAudio
url="fear/snd/riser.mp3"
url='fear/snd/riser.mp3'
ref={audioRef}
distance={25}
loop={false}
@@ -30,11 +30,9 @@
will-change: filter;
animation: invertFlicker 0.07s infinite alternate;
}
@keyframes invertFlicker {
0%,
43%,
45%,
@@ -66,7 +64,6 @@
text-align: center;
white-space: nowrap;
}
.scanlines {
@@ -76,9 +73,10 @@
width: 100%;
height: 100%;
z-index: 900;
background: repeating-linear-gradient(rgba(0, 0, 0, 0) 0px,
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);
rgba(0, 0, 0, 0.3) 4px
);
}
+52 -22
View File
@@ -1,13 +1,40 @@
import { JSX, useEffect, useState } from "react"
import { fearState } from "../state"
import { JSX, useEffect, useState } from 'react';
import { fearState } from '../state';
import './finale-text.css';
const BLOCKS = [
"▀", "▂", "▃", "▄", "▅", "▆", "▇",
"█", "▉", "▊", "▋", "▌", "▍", "▎", "▏",
"▐", "░", "▒", "▓", "▔", "▕", "▖", "▗",
"▘", "▙", "▚", "▛", "▜", "▝", "▞", "▟"
'▀',
'▂',
'▃',
'▄',
'▅',
'▆',
'▇',
'█',
'▉',
'▊',
'▋',
'▌',
'▍',
'▎',
'▏',
'▐',
'░',
'▒',
'▓',
'▔',
'▕',
'▖',
'▗',
'▘',
'▙',
'▚',
'▛',
'▜',
'▝',
'▞',
'▟'
];
export default function FinaleText() {
@@ -16,27 +43,30 @@ export default function FinaleText() {
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setWasCaught(fearState.wasCaught)
setWasCaught(fearState.wasCaught);
});
return () => unsubscribe();
}, []);
useEffect(() => {
if (!wasCaught)
return;
if (!wasCaught) return;
const interval = setInterval(() => {
if (Math.random() > 0.9) return;
const baseText = "bwaaaaaaaaa";
const baseText = 'bwaaaaaaaaa';
const corrupted = baseText
.split("")
.map((char) => (Math.random() > 0.98 ? BLOCKS[Math.floor(Math.random() * BLOCKS.length)] : char))
.join("");
.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()}>
setElements((prev) => [
...prev.slice(-30),
<span className='finale-text' key={crypto.randomUUID()}>
{corrupted}
</span>
]);
@@ -47,10 +77,10 @@ export default function FinaleText() {
if (!wasCaught) return null;
return (<>
<div className="finale-container">
{elements}
</div>
<div className="scanlines" />
</>)
return (
<>
<div className='finale-container'>{elements}</div>
<div className='scanlines' />
</>
);
}
+193 -62
View File
@@ -1,10 +1,10 @@
import { useEffect, useRef, useState } from "react";
import { FEAR_SETTINGS, fearState } from "../state";
import { useTexture, PositionalAudio } from "@react-three/drei";
import { useEffect, useRef, useState } from 'react';
import { FEAR_SETTINGS, fearState } from '../state';
import { useTexture, PositionalAudio } from '@react-three/drei';
import * as THREE from "three";
import { useFrame } from "@react-three/fiber";
import { ShaderPatch } from "../shader-patch";
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
import { ShaderPatch } from '../shader-patch';
interface DoorProps {
position: [number, number, number];
@@ -18,7 +18,8 @@ function Door({ position, rotation }: DoorProps) {
useEffect(() => {
const interval = setInterval(() => {
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);
currentSound.current = chosenSound;
@@ -38,19 +39,31 @@ function Door({ position, rotation }: DoorProps) {
{/* frame */}
<mesh position={[0, 2, -0.1]}>
<boxGeometry args={[2.4, 4.0, 0.2, 4, 4, 1]} />
<meshStandardMaterial map={steelTex} color="#8d8d8d" onBeforeCompile={ShaderPatch} />
<meshStandardMaterial
map={steelTex}
color='#8d8d8d'
onBeforeCompile={ShaderPatch}
/>
</mesh>
{/* panel */}
<mesh position={[0, 1.95, -0.0]}>
<boxGeometry args={[2.1, 3.8, 0.1, 4, 4, 1]} />
<meshStandardMaterial map={steelTex} color="#4e4a4a" onBeforeCompile={ShaderPatch} />
<meshStandardMaterial
map={steelTex}
color='#4e4a4a'
onBeforeCompile={ShaderPatch}
/>
</mesh>
{/* handle */}
<mesh position={[0.75, 1.8, .085]}>
<mesh position={[0.75, 1.8, 0.085]}>
<boxGeometry args={[0.3, 0.08, 0.1]} />
<meshStandardMaterial map={steelTex} color="#ffffff" onBeforeCompile={ShaderPatch} />
<meshStandardMaterial
map={steelTex}
color='#ffffff'
onBeforeCompile={ShaderPatch}
/>
</mesh>
{soundUrl && (
@@ -83,7 +96,6 @@ export default function Hallway() {
});
}, [floorTex, wallTex, rustWallTex, rustFloorTex]);
const segmentPool = [0, 1, 2, 3, 4];
const segmentCount = segmentPool.length;
@@ -138,7 +150,8 @@ export default function Hallway() {
} else {
const baseWave = Math.sin(time * 45) * 0.4 + Math.sin(time * 90) * 0.3;
intensity1 = 0.5 + baseWave;
if (Math.sin(time * 150) + Math.cos(time * 220) > 1.2) intensity1 *= Math.random() > 0.5 ? 0.0 : 0.15;
if (Math.sin(time * 150) + Math.cos(time * 220) > 1.2)
intensity1 *= Math.random() > 0.5 ? 0.0 : 0.15;
}
}
if (lightState.current === 'dead') {
@@ -173,7 +186,8 @@ export default function Hallway() {
segmentsRef.current.forEach((segGroup, poolIndex) => {
if (!segGroup) return;
let segmentZIndex = poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ;
let segmentZIndex =
poolIndex - Math.floor(segmentCount / 2) + playerSegmentZ;
segGroup.position.z = segmentZIndex * length;
const distance = Math.abs(segGroup.position.z - state.camera.position.z);
@@ -182,25 +196,26 @@ export default function Hallway() {
closestPoolIndex = poolIndex;
}
const leftWallGroup = segGroup.getObjectByName("left-wall-group");
const leftWallGroup = segGroup.getObjectByName('left-wall-group');
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;
const floorMesh = segGroup.getObjectByName("floor-mesh");
const floorMesh = segGroup.getObjectByName('floor-mesh');
if (floorMesh) floorMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH;
const ceilingMesh = segGroup.getObjectByName("ceiling-mesh");
if (ceilingMesh) ceilingMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH;
const ceilingMesh = segGroup.getObjectByName('ceiling-mesh');
if (ceilingMesh)
ceilingMesh.scale.x = width / FEAR_SETTINGS.HALLWAY_WIDTH;
for (let i = 0; i < 3; i++) {
const pipe = segGroup.getObjectByName(`pipe-${i}`);
if (pipe) pipe.position.x = -width / 2 + 0.4 + (i * 0.20);
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) {
bracketGroup.children.forEach(b => {
bracketGroup.children.forEach((b) => {
b.position.x = -width / 2 + 0.6;
});
}
@@ -217,7 +232,8 @@ export default function Hallway() {
if (light) light.intensity = intensity1 * 1.2;
if (mat) {
mat.emissiveIntensity = intensity1 * 2.5;
if (lightState.current !== 'normal') mat.emissive.setHSL(0.07, 0.4, Math.min(intensity1, 0.7));
if (lightState.current !== 'normal')
mat.emissive.setHSL(0.07, 0.4, Math.min(intensity1, 0.7));
else mat.emissive.setHex(0xa8a1a1);
}
} else {
@@ -232,8 +248,18 @@ export default function Hallway() {
/*
materials
*/
const updateMaterials = (materials: THREE.MeshStandardMaterial[], defaultTex: THREE.Texture, targetRustTex: THREE.Texture, activeColor: string, defaultColor: string, activeRough: number, defaultRough: number, activeMetal: number, defaultMetal: number) => {
materials.forEach(mat => {
const updateMaterials = (
materials: THREE.MeshStandardMaterial[],
defaultTex: THREE.Texture,
targetRustTex: THREE.Texture,
activeColor: string,
defaultColor: string,
activeRough: number,
defaultRough: number,
activeMetal: number,
defaultMetal: number
) => {
materials.forEach((mat) => {
if (!mat) return;
const targetTex = isRustActive ? targetRustTex : defaultTex;
if (mat.map !== targetTex) {
@@ -246,19 +272,39 @@ export default function Hallway() {
});
};
updateMaterials(wallMaterialsRef.current, wallTex, rustWallTex, "#c5c0be", "#ffffff", 0.95, 0.7, 0.05, 0.1);
updateMaterials(floorMaterialsRef.current, floorTex, rustFloorTex, "#cabdb9", "#ffffff", 0.95, 0.8, 0.05, 0.2);
updateMaterials(
wallMaterialsRef.current,
wallTex,
rustWallTex,
'#c5c0be',
'#ffffff',
0.95,
0.7,
0.05,
0.1
);
updateMaterials(
floorMaterialsRef.current,
floorTex,
rustFloorTex,
'#cabdb9',
'#ffffff',
0.95,
0.8,
0.05,
0.2
);
pipeMaterialsRef.current.forEach(mat => {
pipeMaterialsRef.current.forEach((mat) => {
if (!mat) return;
mat.color.set(isRustActive ? "#3d1b0f" : "#a5aca8");
mat.color.set(isRustActive ? '#3d1b0f' : '#a5aca8');
mat.roughness = isRustActive ? 0.95 : 0.0;
mat.metalness = isRustActive ? 0.05 : 0.4;
});
bracketMaterialsRef.current.forEach(mat => {
bracketMaterialsRef.current.forEach((mat) => {
if (!mat) return;
mat.color.set(isRustActive ? "#1b0b05" : "#a5aca8");
mat.color.set(isRustActive ? '#1b0b05' : '#a5aca8');
mat.roughness = isRustActive ? 0.95 : 0.0;
mat.metalness = isRustActive ? 0.05 : 0.4;
});
@@ -269,23 +315,35 @@ export default function Hallway() {
{segmentPool.map((poolIndex) => (
<group
key={poolIndex}
ref={(el) => { if (el) segmentsRef.current[poolIndex] = el; }}
ref={(el) => {
if (el) segmentsRef.current[poolIndex] = el;
}}
position={[0, 0, 0]}
>
{/* 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
ref={(el) => { lightRefs.current[poolIndex] = el; }}
ref={(el) => {
lightRefs.current[poolIndex] = el;
}}
intensity={0.9}
distance={15}
color="#a8a1a1"
color='#a8a1a1'
/>
<mesh position={[0, 0.09, 0]}>
<boxGeometry args={[0.3, 0.01, 0.3]} />
<meshStandardMaterial
ref={(el) => { matRefs.current[poolIndex] = el; }}
color="#111111"
emissive="#a8a1a1"
ref={(el) => {
matRefs.current[poolIndex] = el;
}}
color='#111111'
emissive='#a8a1a1'
emissiveIntensity={0.8}
roughness={0.9}
onBeforeCompile={ShaderPatch}
@@ -295,13 +353,22 @@ export default function Hallway() {
{/* floor */}
<mesh
name="floor-mesh"
name='floor-mesh'
rotation={[-Math.PI / 2, 0, 0]}
position={[0, 0, -FEAR_SETTINGS.HALLWAY_LENGTH / 2]}
>
<planeGeometry args={[FEAR_SETTINGS.HALLWAY_WIDTH, FEAR_SETTINGS.HALLWAY_LENGTH, 4, 10]} />
<planeGeometry
args={[
FEAR_SETTINGS.HALLWAY_WIDTH,
FEAR_SETTINGS.HALLWAY_LENGTH,
4,
10
]}
/>
<meshStandardMaterial
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
ref={(el) => {
if (el) floorMaterialsRef.current.push(el);
}}
map={floorTex}
onBeforeCompile={ShaderPatch}
/>
@@ -309,48 +376,102 @@ export default function Hallway() {
{/* ceiling */}
<mesh
name="ceiling-mesh"
name='ceiling-mesh'
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, 4, 10]} />
<planeGeometry
args={[
FEAR_SETTINGS.HALLWAY_WIDTH,
FEAR_SETTINGS.HALLWAY_LENGTH,
4,
10
]}
/>
<meshStandardMaterial
ref={(el) => { if (el) floorMaterialsRef.current.push(el); }}
ref={(el) => {
if (el) floorMaterialsRef.current.push(el);
}}
map={floorTex}
onBeforeCompile={ShaderPatch}
/>
</mesh>
{/* left wall */}
<group name="left-wall-group">
<mesh 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]} />
<group name='left-wall-group'>
<mesh
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
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
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]} />
<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 */}
<group name="right-wall-group">
<mesh 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]} />
<group name='right-wall-group'>
<mesh
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
ref={(el) => { if (el) wallMaterialsRef.current.push(el); }}
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]} />
<Door
position={[-0.05, 0, -FEAR_SETTINGS.HALLWAY_LENGTH * 0.65]}
rotation={[0, -Math.PI / 2, 0]}
/>
)}
</group>
@@ -360,12 +481,18 @@ export default function Hallway() {
key={idx}
name={`pipe-${idx}`}
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
ref={(el) => el && pipeMaterialsRef.current.push(el)}
color="#a5aca8"
color='#a5aca8'
roughness={0.0}
metalness={0.4}
onBeforeCompile={ShaderPatch}
@@ -374,18 +501,22 @@ export default function Hallway() {
))}
{/* brackets */}
<group name="brackets-group">
<group name='brackets-group'>
{Array.from({ length: 5 }).map((_, idx) => {
const zOffset = -(idx * 8 + 4);
return (
<mesh
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]} />
<meshStandardMaterial
ref={(el) => el && bracketMaterialsRef.current.push(el)}
color="#a5aca8"
color='#a5aca8'
roughness={0.0}
metalness={0.4}
onBeforeCompile={ShaderPatch}
+61 -33
View File
@@ -1,8 +1,8 @@
import { useFrame, useThree } from "@react-three/fiber";
import { useEffect, useRef } from "react";
import { FEAR_SETTINGS, fearState } from "../state";
import { PointerLockControls } from "@react-three/drei";
import * as THREE from "three";
import { useFrame, useThree } from '@react-three/fiber';
import { useEffect, useRef } from 'react';
import { FEAR_SETTINGS, fearState } from '../state';
import { PointerLockControls } from '@react-three/drei';
import * as THREE from 'three';
const forward = new THREE.Vector3();
const side = new THREE.Vector3();
@@ -14,21 +14,33 @@ const targetVelocity = new THREE.Vector3();
const currentVelocity = new THREE.Vector3();
function usePlayerControls() {
const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false });
const keys = useRef({
Forward: false,
Backward: false,
Left: false,
Right: false
});
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = true;
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = true;
if (e.code === 'KeyW' || e.code === 'ArrowUp')
keys.current.Forward = true;
if (e.code === 'KeyS' || e.code === 'ArrowDown')
keys.current.Backward = true;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = true;
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = true;
if (e.code === 'KeyD' || e.code === 'ArrowRight')
keys.current.Right = true;
};
const handleKeyUp = (e: KeyboardEvent) => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = false;
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = false;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = false;
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = false;
if (e.code === 'KeyW' || e.code === 'ArrowUp')
keys.current.Forward = false;
if (e.code === 'KeyS' || e.code === 'ArrowDown')
keys.current.Backward = false;
if (e.code === 'KeyA' || e.code === 'ArrowLeft')
keys.current.Left = false;
if (e.code === 'KeyD' || e.code === 'ArrowRight')
keys.current.Right = false;
};
window.addEventListener('keydown', handleKeyDown);
@@ -57,10 +69,12 @@ export default function Player() {
const footstepAudio = useRef<HTMLAudioElement[]>([]);
const hasStepped = useRef<boolean>(false);
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;
@@ -71,12 +85,17 @@ export default function Player() {
const playRandomFootstep = () => {
if (footstepAudio.current.length === 0) return;
const randomIndex = Math.floor(Math.random() * footstepAudio.current.length);
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);
console.warn(
'Footstep playback blocked by browser autocomplete/interaction rules.',
err
);
});
};
@@ -107,16 +126,21 @@ export default function Player() {
const maxX = fearState.currentWidth / 2 - FEAR_SETTINGS.WALL_BUFFER;
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)
movementCounter.current += dt * 12;
if (isMoving) movementCounter.current += dt * 12;
const sinWave = Math.sin(movementCounter.current);
const moveBobY = sinWave * 0.06 * bobIntensity.current;
const moveBobX = Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current;
const moveBobX =
Math.cos(movementCounter.current / 2) * 0.04 * bobIntensity.current;
if (isMoving && sinWave < -0.9) {
if (!hasStepped.current) {
@@ -128,7 +152,8 @@ export default function Player() {
}
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.y += moveBobY + breatheBobY;
@@ -138,22 +163,22 @@ export default function Player() {
flashlightRef.current.position.lerp(camera.position, 7 * dt);
camera.getWorldDirection(viewDirection);
targetDest
.copy(camera.position)
.addScaledVector(viewDirection, 10);
targetDest.copy(camera.position).addScaledVector(viewDirection, 10);
flashlightRef.current.target.position.lerp(targetDest, 12 * dt);
flashlightRef.current.target.updateMatrixWorld();
flashlightRef.current.intensity =
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 absoluteZ = -playerRoot.z;
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 (!hasTriggeredThisSegment.current) {
@@ -161,8 +186,7 @@ export default function Player() {
hasTriggeredThisSegment.current = true;
}
confirmedSegment.current = rawSegmentIndex;
}
else if (rawSegmentIndex < confirmedSegment.current && progressZ < 0.75) {
} else if (rawSegmentIndex < confirmedSegment.current && progressZ < 0.75) {
if (!hasTriggeredThisSegment.current) {
fearState.registerLoop('backward');
hasTriggeredThisSegment.current = true;
@@ -170,7 +194,11 @@ export default function Player() {
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;
}
});
@@ -184,7 +212,7 @@ export default function Player() {
angle={0.35}
penumbra={0.8}
intensity={0}
color="#fffaed"
color='#fffaed'
decay={2}
castShadow
shadow-bias={-0.001}
+5 -1
View File
@@ -1,4 +1,8 @@
export function ShaderPatch(shader: { vertexShader: string, fragmentShader: string, uniforms: Object }) {
export function ShaderPatch(shader: {
vertexShader: string;
fragmentShader: string;
uniforms: Object;
}) {
shader.vertexShader = `
varying float vDepth;
#ifdef USE_MAP
+18 -6
View File
@@ -31,7 +31,9 @@ export const fearState = {
subscribe(listener: () => void) {
listeners.add(listener);
return () => { listeners.delete(listener); };
return () => {
listeners.delete(listener);
};
},
emit() {
@@ -39,8 +41,15 @@ export const fearState = {
},
update(delta: number) {
const targetWidth = this.loopCount >= FEAR_SETTINGS.EVENT_NARROW_LOOP_COUNT ? 2.5 : FEAR_SETTINGS.HALLWAY_WIDTH;
const newWidth = THREE.MathUtils.lerp(this.currentWidth, targetWidth, 2 * delta);
const targetWidth =
this.loopCount >= FEAR_SETTINGS.EVENT_NARROW_LOOP_COUNT
? 2.5
: FEAR_SETTINGS.HALLWAY_WIDTH;
const newWidth = THREE.MathUtils.lerp(
this.currentWidth,
targetWidth,
2 * delta
);
if (Math.abs(this.currentWidth - newWidth) > 0.001) {
this.currentWidth = newWidth;
@@ -48,8 +57,10 @@ export const fearState = {
if (this.wasCaught) {
if (this.finaleProgression < FEAR_SETTINGS.EVENT_FINALE_DURATION) {
this.finaleProgression = Math.min(this.finaleProgression + delta, FEAR_SETTINGS.EVENT_FINALE_DURATION);
this.finaleProgression = Math.min(
this.finaleProgression + delta,
FEAR_SETTINGS.EVENT_FINALE_DURATION
);
} else {
window.location.href = '/';
}
@@ -62,7 +73,8 @@ export const fearState = {
this.loopCount += 1;
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();
},
+28 -16
View File
@@ -2,10 +2,21 @@
import './page.css';
import { Environment, OrbitControls, useProgress } from "@react-three/drei";
import { Environment, OrbitControls, useProgress } from '@react-three/drei';
import { Canvas, useLoader } from '@react-three/fiber';
import { Bloom, BrightnessContrast, DepthOfField, EffectComposer, HueSaturation, LUT, Noise, SMAA, SSAO, Vignette } from '@react-three/postprocessing';
import { useLayoutEffect, useState } from "react";
import {
Bloom,
BrightnessContrast,
DepthOfField,
EffectComposer,
HueSaturation,
LUT,
Noise,
SMAA,
SSAO,
Vignette
} from '@react-three/postprocessing';
import { useLayoutEffect, useState } from 'react';
import { folder, useControls, Leva } from 'leva';
import SealCube from './scene-components/sealcube';
import Terrain from './scene-components/terrain';
@@ -62,7 +73,7 @@ function Scene() {
hillScale: { value: 0.15, min: 0.01, max: 0.5, step: 0.01 },
hillHeight: { value: 4.0, min: 0.0, max: 20.0, step: 0.5 },
detailScale: { value: 1.0, min: 0.1, max: 5.0, step: 0.1 },
detailHeight: { value: 0.3, min: 0.0, max: 2.0, step: 0.05 },
detailHeight: { value: 0.3, min: 0.0, max: 2.0, step: 0.05 }
}),
Grass: folder({
grassDryColor: '#495a17',
@@ -73,11 +84,12 @@ function Scene() {
grassBlades: { value: 3, min: 1, max: 5, step: 1 },
grassSegments: { value: 4, min: 1, max: 5, step: 1 },
grassLODStart: { value: 0.15, min: 0.0, max: 0.9, step: 0.05 },
grassLODExponent: { value: 1.8, min: 0.5, max: 3.0, step: 0.1 },
grassLODExponent: { value: 1.8, min: 0.5, max: 3.0, step: 0.1 }
})
});
return (<>
return (
<>
<Environment
files={'niko/hdr/sky.hdr'}
environmentIntensity={0.85}
@@ -87,10 +99,7 @@ function Scene() {
<fogExp2 attach='fog' args={[0xa3a5ba, 0.0125]} />
<ambientLight intensity={0.5} />
<directionalLight
position={[15, 25, 15]}
intensity={1}
/>
<directionalLight position={[15, 25, 15]} intensity={1} />
<Terrain
chunks={chunks}
@@ -115,7 +124,8 @@ function Scene() {
/>
<SealCube />
</>)
</>
);
}
function LutEffect() {
@@ -124,7 +134,8 @@ function LutEffect() {
}
function PostProcessing() {
return (<EffectComposer>
return (
<EffectComposer>
<DepthOfField target={[0, 3, 0]} focalLength={10} bokehScale={5} />
<Vignette />
<Noise opacity={0.05} />
@@ -137,7 +148,8 @@ function PostProcessing() {
<HueSaturation saturation={0.3} />
<BrightnessContrast brightness={0.05} contrast={-0.1} />
<LutEffect />
</EffectComposer>)
</EffectComposer>
);
}
export default function Seal() {
@@ -151,11 +163,11 @@ export default function Seal() {
<Canvas
shadows
camera={{ position: [0, 5, 15], fov: 50, far: 100 }}
gl={{ antialias: false, powerPreference: "high-performance" }}
gl={{ antialias: false, powerPreference: 'high-performance' }}
className='canvas'
>
<AmbientSound url="niko/snd/wind.mp3" volume={0.4} />
<AmbientSound url="niko/snd/birds.mp3" volume={0.1} />
<AmbientSound url='niko/snd/wind.mp3' volume={0.4} />
<AmbientSound url='niko/snd/birds.mp3' volume={0.1} />
<Scene />
<PostProcessing />
+23 -23
View File
@@ -1,43 +1,43 @@
import { useEffect, useRef } from 'react'
import { useEffect, useRef } from 'react';
interface AmbientSoundProps {
url: string
volume?: number
url: string;
volume?: number;
}
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
const audioRef = useRef<HTMLAudioElement | null>(null)
const audioRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
const audio = new Audio(url)
audio.loop = true
audio.volume = volume
audioRef.current = audio
const audio = new Audio(url);
audio.loop = true;
audio.volume = volume;
audioRef.current = audio;
const startAudio = () => {
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('keydown', startAudio, { once: true })
window.addEventListener('click', startAudio, { once: true });
window.addEventListener('keydown', startAudio, { once: true });
return () => {
window.removeEventListener('click', startAudio)
window.removeEventListener('keydown', startAudio)
audio.pause()
audioRef.current = null
}
}, [url])
window.removeEventListener('click', startAudio);
window.removeEventListener('keydown', startAudio);
audio.pause();
audioRef.current = null;
};
}, [url]);
useEffect(() => {
if (audioRef.current) {
audioRef.current.volume = volume
audioRef.current.volume = volume;
}
}, [volume])
}, [volume]);
return null
return null;
}
+15 -5
View File
@@ -1,7 +1,16 @@
import { useFrame, useLoader } from "@react-three/fiber";
import { useLayoutEffect, useMemo, useRef } from "react";
import { BufferAttribute, BufferGeometry, Color, DoubleSide, InstancedMesh, MeshStandardMaterial, Object3D, TextureLoader } from "three";
import { getTerrainHeight, Shader } from "./helpers";
import { useFrame, useLoader } from '@react-three/fiber';
import { useLayoutEffect, useMemo, useRef } from 'react';
import {
BufferAttribute,
BufferGeometry,
Color,
DoubleSide,
InstancedMesh,
MeshStandardMaterial,
Object3D,
TextureLoader
} from 'three';
import { getTerrainHeight, Shader } from './helpers';
import grassVert from './shaders/grass.vert';
import grassFrag from './shaders/grass.frag';
@@ -187,7 +196,8 @@ export default function Grass({
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);
const grassHeight =
grassSize * (0.4 + macroHeight * 0.8 + microHeight + perBladeRandom);
dummy.scale.set(grassWidth, grassHeight, grassWidth);
+5 -4
View File
@@ -1,6 +1,6 @@
import { useFrame, useLoader } from "@react-three/fiber";
import { forwardRef, useImperativeHandle, useRef } from "react";
import { Mesh, TextureLoader } from "three";
import { useFrame, useLoader } from '@react-three/fiber';
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { Mesh, TextureLoader } from 'three';
const SealCube = forwardRef<Mesh>((props, ref) => {
const texture = useLoader(TextureLoader, 'niko/img/niko.jpg');
@@ -12,7 +12,8 @@ const SealCube = forwardRef<Mesh>((props, ref) => {
if (meshRef.current) {
meshRef.current.rotation.x += 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;
}
});
+9 -7
View File
@@ -1,8 +1,8 @@
import { useMemo, useRef } from "react";
import { BufferAttribute, BufferGeometry, Color, Mesh } from "three";
import { getTerrainHeight } from "./helpers";
import Grass from "./grass";
import { createNoise2D } from "simplex-noise";
import { useMemo, useRef } from 'react';
import { BufferAttribute, BufferGeometry, Color, Mesh } from 'three';
import { getTerrainHeight } from './helpers';
import Grass from './grass';
import { createNoise2D } from 'simplex-noise';
interface TerrainChunkProps {
x: number;
@@ -54,8 +54,10 @@ function TerrainChunk({
const worldXBase = x * size;
const worldZBase = y * size;
const minX = Math.abs(worldXBase) <= halfSize ? 0 : Math.abs(worldXBase) - halfSize;
const minZ = Math.abs(worldZBase) <= halfSize ? 0 : Math.abs(worldZBase) - halfSize;
const minX =
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 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 {
color: var(--text-main);
font-family: "Times New Roman", Times, serif;
font-family: 'Times New Roman', Times, serif;
text-align: center;
display: flex;
@@ -68,7 +70,9 @@ body {
border: 1px solid var(--pink-accent);
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;
animation: float 5s ease-in-out infinite;
@@ -79,14 +83,20 @@ body {
top: 10px;
font-size: 1.5rem;
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;
align-items: center;
justify-content: center;
line-height: 1;
height: 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;
}
@@ -104,7 +114,7 @@ body {
}
.content-box::before {
content: "•";
content: '•';
position: absolute;
top: -4px;
left: 6px;
@@ -204,15 +214,17 @@ h1 {
}
.directory a::before {
content: " "
content: ' ';
}
.directory a::after {
content: " "
content: ' ';
}
.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);
box-shadow: 1px 1px 0px var(--pink-accent);
background: var(--pink-accent);
@@ -234,7 +246,12 @@ h1 {
border-top: 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 {
+120 -46
View File
@@ -4,10 +4,10 @@ import { useEffect, useState } from 'react';
import './page.css';
import { DiscordStatus } from './components/discordstatus';
const TWITTER_LINK = "https://x.com/neruu444"
const DISCORD_USER = "neru444"
const DISCORD_ID = "1104474057916809226"
const STEAM_LINK = "https://steamcommunity.com/profiles/76561198440714757/"
const TWITTER_LINK = 'https://x.com/neruu444';
const DISCORD_USER = 'neru444';
const DISCORD_ID = '1104474057916809226';
const STEAM_LINK = 'https://steamcommunity.com/profiles/76561198440714757/';
function Content() {
const [isOpen, setIsOpen] = useState(false);
@@ -16,66 +16,136 @@ function Content() {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen)
setIsOpen(false);
if (e.key === 'Escape' && isOpen) setIsOpen(false);
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen]);
const marqueeText = "✧ ꒰ა˵• ﻌ •˵ა꒱ ✧ ฅ^•ﻌ•^ฅ ✧ ᶻ 𝗓 𐰁 /ᐠ. 。 .ᐟ\\ ✧ ฅ/ᐠ. ̫ .ᐟ\\ฅ ✧ ꒰ა≽^•⩊•^≼໒꒱ ✧ ₍˄·͈༝·͈˄₎ ✧ /ᐠ. ⩊ .ᐟ\\ノ ✧ 𓏲ּ ֶָ ࣪ /ᐠ .ᆺ. ᐟ\\ノ ✧";
const marqueeText =
'✧ ꒰ა˵• ﻌ •˵ა꒱ ✧ ฅ^•ﻌ•^ฅ ✧ ᶻ 𝗓 𐰁 /ᐠ. 。 .ᐟ\\ ✧ ฅ/ᐠ. ̫ .ᐟ\\ฅ ✧ ꒰ა≽^•⩊•^≼໒꒱ ✧ ₍˄·͈༝·͈˄₎ ✧ /ᐠ. ⩊ .ᐟ\\ノ ✧ 𓏲ּ ֶָ ࣪ /ᐠ .ᆺ. ᐟ\\ノ ✧';
return (
<>
<div className="main-frame">
<a href="/niko" className="decorative-sparkle" title="✧" style={{ left: '10px' }}></a>
<a href="/fear" className="decorative-sparkle" title="✧" style={{ right: '10px' }}></a>
<div className='main-frame'>
<a
href='/niko'
className='decorative-sparkle'
title='✧'
style={{ left: '10px' }}
>
</a>
<a
href='/fear'
className='decorative-sparkle'
title='✧'
style={{ right: '10px' }}
>
</a>
<header>
<h1>neru</h1>
<p className="motto">˚ 𓂋 ˚</p>
<p className='motto'>˚ 𓂋 ˚</p>
</header>
<nav className="social-links">
<a href={TWITTER_LINK} target="_blank" rel="noopener noreferrer">twitter</a>
<button onClick={toggleModal}>discord</button>
<a href={STEAM_LINK} target="_blank" rel="noopener noreferrer">steam</a>
<nav className='social-links'>
<a href={TWITTER_LINK} target='_blank' rel='noopener noreferrer'>
twitter
</a>{' '}
<button onClick={toggleModal}>discord</button>
<a href={STEAM_LINK} target='_blank' rel='noopener noreferrer'>
steam
</a>
</nav>
<section className="content-box">
<h2 className="title"> discord </h2>
<section className='content-box'>
<h2 className='title'> discord </h2>
<DiscordStatus userId={DISCORD_ID} />
</section>
<section className="content-box">
<h2 className="title"> projects im currently working on </h2>
<ul className="directory">
<li><a 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>
<section className='content-box'>
<h2 className='title'> projects im currently working on </h2>
<ul className='directory'>
<li>
<a
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>
</section>
<section className="content-box">
<h2 className="title"> sites </h2>
<ul className="directory">
<li><a 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>
<section className='content-box'>
<h2 className='title'> sites </h2>
<ul className='directory'>
<li>
<a
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>
</section>
<section className="content-box">
<h2 className="title"> dumb stuff </h2>
<ul className="directory">
<li><a href="discord://-/apps">break discord</a></li>
<li><a href="discord://-/channels/@me/">fix discord</a></li>
<section className='content-box'>
<h2 className='title'> dumb stuff </h2>
<ul className='directory'>
<li>
<a href='discord://-/apps'>break discord</a>
</li>
<li>
<a href='discord://-/channels/@me/'>fix discord</a>
</li>
</ul>
</section>
<footer>
<div className="marquee">
<div className="marquee-track">
<div className='marquee'>
<div className='marquee-track'>
<span>{marqueeText}</span>
<span>{marqueeText}</span>
</div>
@@ -83,14 +153,20 @@ function Content() {
</footer>
{isOpen && (
<div className="modal-overlay" onClick={toggleModal}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header"> discord info </div>
<div className="modal-body">
<p><strong>User:</strong> {DISCORD_USER}</p>
<p><strong>ID:</strong> {DISCORD_ID}</p>
<div className='modal-overlay' onClick={toggleModal}>
<div className='modal-content' onClick={(e) => e.stopPropagation()}>
<div className='modal-header'> discord info </div>
<div className='modal-body'>
<p>
<strong>User:</strong> {DISCORD_USER}
</p>
<p>
<strong>ID:</strong> {DISCORD_ID}
</p>
</div>
<button className="modal-close-btn" onClick={toggleModal}>[ close ]</button>
<button className='modal-close-btn' onClick={toggleModal}>
[ close ]
</button>
</div>
</div>
)}
@@ -100,7 +176,5 @@ function Content() {
}
export default function Home() {
return (
<Content />
)
return <Content />;
}