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