Compare commits

...

2 Commits

Author SHA1 Message Date
neru 8c4080f10c feat: add finale text 2026-06-01 22:02:22 -03:00
neru b9eeed848b feat: add door knocks 2026-06-01 22:02:12 -03:00
8 changed files with 134 additions and 4 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4 -1
View File
@@ -15,6 +15,7 @@ import Player from './scene-components/player';
import Hallway from './scene-components/hallway';
import { AudioListener } from 'three';
import FinaleText from './scene-components/finale-text';
function PostProcessing() {
const [wasCaught, setWasCaught] = useState(fearState.wasCaught);
@@ -94,7 +95,7 @@ export default function Fear() {
<TheCreature />
<Player />
</Suspense>
<AmbientSound
key="ambient-1"
url='fear/snd/ambience.mp3'
@@ -113,5 +114,7 @@ export default function Fear() {
volume={1}
/> : null}
</Canvas>
<FinaleText />
</>)
}
@@ -0,0 +1,52 @@
@font-face {
font-family: 'VCR';
src: url('/fear/fonts/vcr.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
.finale-container {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0vh;
display: grid;
align-items: center;
align-content: center;
justify-content: center;
overflow: hidden;
grid-auto-rows: 2.5vh;
/* grid-template-columns: 0; */
grid-template-rows: repeat(auto-fit, max-content);
user-select: none;
}
.finale-text {
font-family: 'VCR', sans-serif;
height: 0px;
width: 100%;
color: rgb(255, 255, 255);
font-size: 5vh;
text-align: center;
white-space: nowrap;
}
.scanlines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 900;
background: repeating-linear-gradient(rgba(0, 0, 0, 0) 0px,
rgba(0, 0, 0, 0) 2px,
rgba(0, 0, 0, 0.3) 2px,
rgba(0, 0, 0, 0.3) 4px);
}
@@ -0,0 +1,36 @@
import { JSX, useEffect, useState } from "react"
import { FEAR_SETTINGS, fearState } from "../state"
import './finale-text.css';
export default function FinaleText() {
const [progression, setProgression] = useState(fearState.finaleProgression);
const [wasCaught, setWasCaught] = useState(fearState.isRustActive);
useEffect(() => {
const unsubscribe = fearState.subscribe(() => {
setProgression(fearState.finaleProgression);
setWasCaught(fearState.wasCaught)
});
return () => unsubscribe();
});
let elementCount = (FEAR_SETTINGS.EVENT_FINALE_TEXT_COUNT / FEAR_SETTINGS.EVENT_FINALE_DURATION) * progression;
let testElements: Array<JSX.Element> = [];
for (let x = 0; x < elementCount; x++)
testElements.push(<span className="finale-text" key={x}>the deal has been sealed</span>)
if (wasCaught)
return (<>
<div className="finale-container">
{testElements}
</div>
<div className="scanlines" />
</>)
return <></>
}
+32 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from "react";
import { FEAR_SETTINGS, fearState } from "../state";
import { useTexture } from "@react-three/drei";
import { useTexture, PositionalAudio } from "@react-three/drei";
import * as THREE from "three";
import { useFrame } from "@react-three/fiber";
@@ -10,6 +10,27 @@ interface DoorProps {
rotation: [number, number, number];
}
function Door({ position, rotation }: DoorProps) {
const [soundUrl, setSoundUrl] = useState<string | null>(null);
const currentSound = useRef<string | null>(null);
useEffect(() => {
const interval = setInterval(() => {
if (Math.random() < 0.02) {
const chosenSound = Math.random() < 0.5 ? "fear/snd/knock1.mp3" : "fear/snd/knock2.mp3";
setSoundUrl(chosenSound);
currentSound.current = chosenSound;
}
}, 5000);
return () => clearInterval(interval);
}, []);
const handleAudioEnded = () => {
setSoundUrl(null);
currentSound.current = null;
};
return (
<group position={position} rotation={rotation}>
<mesh position={[0, 2, -0.14]}>
@@ -26,6 +47,16 @@ function Door({ position, rotation }: DoorProps) {
<boxGeometry args={[0.08, 0.08, 0.15]} />
<meshStandardMaterial color="#4e4b4b" roughness={0.4} metalness={0.2} />
</mesh>
{soundUrl && (
<PositionalAudio
url={soundUrl}
distance={25}
loop={false}
autoplay={true}
onEnded={handleAudioEnded}
/>
)}
</group>
);
}
+10 -2
View File
@@ -12,7 +12,10 @@ export const FEAR_SETTINGS = {
EVENT_NARROW_LOOP_COUNT: 2,
EVENT_RUST_LOOP_COUNT: 4,
EVENT_FINALE_LOOP_COUNT: 5
EVENT_FINALE_LOOP_COUNT: 5,
EVENT_FINALE_DURATION: 3,
EVENT_FINALE_TEXT_COUNT: 128
};
const listeners = new Set<() => void>();
@@ -23,6 +26,7 @@ export const fearState = {
isRustActive: false,
finaleTriggered: false,
wasCaught: false,
finaleProgression: 0,
subscribe(listener: () => void) {
listeners.add(listener);
@@ -39,8 +43,12 @@ export const fearState = {
if (Math.abs(this.currentWidth - newWidth) > 0.001) {
this.currentWidth = newWidth;
this.emit();
}
if (this.wasCaught && this.finaleProgression < FEAR_SETTINGS.EVENT_FINALE_DURATION)
this.finaleProgression = Math.min(this.finaleProgression + delta, FEAR_SETTINGS.EVENT_FINALE_DURATION);
this.emit();
},
registerLoop(direction: 'forward' | 'backward') {