import React, { useState, useEffect, useRef, useCallback } from 'react'; // --- AUDIO ENGINE --- // Generates a low, unsettling drone that shifts based on tension level. class HorrorAudioEngine { constructor() { this.ctx = null; this.oscillator = null; this.lfo = null; this.gainNode = null; this.isInitialized = false; } init() { if (this.isInitialized) return; try { const AudioContext = window.AudioContext || window.webkitAudioContext; this.ctx = new AudioContext(); this.oscillator = this.ctx.createOscillator(); this.lfo = this.ctx.createOscillator(); this.gainNode = this.ctx.createGain(); const lfoGain = this.ctx.createGain(); // Deep, unsettling sine wave this.oscillator.type = 'sine'; this.oscillator.frequency.value = 45; // Very low Hz // LFO for a "breathing" or throbbing effect this.lfo.type = 'sine'; this.lfo.frequency.value = 0.1; lfoGain.gain.value = 5; this.lfo.connect(lfoGain); lfoGain.connect(this.oscillator.frequency); this.gainNode.gain.value = 0; // Start silent this.oscillator.connect(this.gainNode); this.gainNode.connect(this.ctx.destination); this.oscillator.start(); this.lfo.start(); this.isInitialized = true; } catch (e) { console.warn("Audio initialization failed. Playing in silent mode.", e); } } setTension(level) { if (!this.isInitialized || !this.ctx) return; // Level 0 to 10 const normalizedLevel = Math.max(0, Math.min(10, level)); // Increase frequency slightly and volume as tension rises const targetFreq = 45 + (normalizedLevel * 2.5); const targetVolume = normalizedLevel === 0 ? 0 : 0.05 + (normalizedLevel * 0.02); const targetLfoRate = 0.1 + (normalizedLevel * 0.2); this.oscillator.frequency.setTargetAtTime(targetFreq, this.ctx.currentTime, 2); this.gainNode.gain.setTargetAtTime(targetVolume, this.ctx.currentTime, 2); this.lfo.frequency.setTargetAtTime(targetLfoRate, this.ctx.currentTime, 2); } stop() { if (this.gainNode && this.ctx) { this.gainNode.gain.setTargetAtTime(0, this.ctx.currentTime, 1); } } } const audioEngine = new HorrorAudioEngine(); // --- STORY DATA --- const STORY = { title: { text: "You shouldn't have come back here.", tension: 0, choices: [{ label: "WAKE UP", target: "wake" }] }, wake: { text: "The cold bites into your cheek. Concrete. You open your eyes to absolute, suffocating darkness. The air smells of old copper and wet earth. Your head throbs with a dull, rhythmic pain. You need to find her. Lily. The last thing you remember is the screaming.", tension: 1, choices: [ { label: "Feel around the floor", target: "feel_floor" }, { label: "Call out for Lily", target: "call_out" } ] }, call_out: { text: "You open your cracked lips to shout, but your throat is agonizingly dry. A raspy croak escapes. \n\nFrom somewhere in the dark above you, a floorboard groans. A slow, heavy shift of weight. Something is up there. Listening.", tension: 3, choices: [ { label: "Stay completely still", target: "feel_floor" }, { label: "Search the floor blindly", target: "feel_floor" } ] }, feel_floor: { text: "Your fingers trace the freezing concrete. Dust, pebbles, and... a viscous puddle of something sticky. You pull your hand back in disgust. Stretching further, your hand brushes against a cold metal cylinder. A heavy-duty flashlight.", tension: 2, choices: [ { label: "Turn on the flashlight", target: "basement_light" } ] }, basement_light: { text: "Click.\n\nA weak, yellow beam cuts through the gloom, illuminating dancing motes of dust. You are in a basement. The walls are cinderblock, stained with dark, weeping moisture. To your left, a wooden staircase ascends into absolute blackness. To your right, a rusting furnace sits like a sleeping iron beast.", tension: 2, choices: [ { label: "Examine the furnace", target: "furnace" }, { label: "Go up the stairs", target: "kitchen" } ] }, furnace: { text: "The furnace is old, covered in soot. Its heavy iron door is slightly ajar. You shine the light inside. It's filled with ash and charred fragments of... fabric. A tiny, half-melted plastic eye stares back at you from the soot. Lily's doll.\n\nA surge of nausea hits you. The thing that took her was here.", tension: 4, choices: [ { label: "Back away to the stairs", target: "kitchen" } ] }, kitchen: { text: "You slowly crest the stairs and enter the kitchen. The linoleum is peeling. The beam of your flashlight catches a refrigerator left ajar, rotting food spilling onto the floor. \n\nOn the kitchen island sits a wooden knife block. One slot is empty. The large butcher knife is missing. \n\nA smear of dark red streaks across the tiles, leading towards the arched doorway of the living room.", tension: 4, choices: [ { label: "Follow the trail to the living room", target: "living_room" }, { label: "Examine the sink", target: "kitchen_sink" } ] }, kitchen_sink: { text: "The sink is clogged with dark, stagnant water. Floating in the scum is a clump of long, pale hair. It looks like it was ripped out from the root. You clamp a hand over your mouth to stifle a gag. You have to keep moving.", tension: 5, choices: [ { label: "Go to the living room", target: "living_room" } ] }, living_room: { text: "The living room is destroyed. The couch is overturned, its stuffing pulled out like entrails. The TV in the corner is smashed, but the cathode ray tube is somehow emitting a faint, high-pitched whine.\n\nThe blood trail ends abruptly at the center of the room. \n\nThere is a door leading out to the back garden, and the main staircase leading up to the bedrooms.", tension: 5, choices: [ { label: "Go outside to the garden", target: "garden" }, { label: "Go up the main staircase", target: "upstairs_hallway" } ] }, garden: { text: "You push the back door open. Thick, unnatural fog rolls across the dead grass. The skeletal branches of the oak tree claw at the moonless sky.\n\nAt the base of the tree is a mound of freshly turned dirt. Next to it lies a small, pink shoe.", tension: 6, choices: [ { label: "Dig into the mound", target: "garden_dig" }, { label: "Go back inside", target: "living_room_return" } ] }, garden_dig: { text: "You drop the flashlight and tear into the cold earth with your bare hands. The dirt caves under your frantic scraping. You dig until your fingernails crack and bleed. \n\nAbout a foot down, your fingers brush against a solid, wooden box. A buried lockbox. You don't have the key, and it's too heavy to lift. \n\nAs you stare at it, you hear a soft *tap tap tap* coming from the living room window behind you.", tension: 8, choices: [ { label: "Look at the window", target: "garden_window" } ] }, garden_window: { text: "You turn. Standing in the living room, looking out at you through the glass, is a tall figure. It is shrouded in shadow, its face completely obscured by the dark. It tilts its head at an unnatural angle, watching you bleed into the dirt.\n\nYou blink, and it's gone.", tension: 9, choices: [ { label: "Rush back inside", target: "living_room_return" } ] }, living_room_return: { text: "You burst back into the living room, sweeping the flashlight wildly. Empty. The overturned furniture casts long, jagged shadows. Whatever was here has retreated upstairs. The staircase beckons like an open jaw.", tension: 7, choices: [ { label: "Ascend the stairs", target: "upstairs_hallway" } ] }, upstairs_hallway: { text: "The upstairs hallway is a long, narrow tunnel of peeling floral wallpaper. The air here is freezing, and every breath you take hangs in the beam of your flashlight. \n\nThere are three doors. The first is the Bathroom. The second is the Nursery. At the end of the hall is the heavy oak door of the Master Bedroom.", tension: 6, choices: [ { label: "Enter the Bathroom", target: "bathroom" }, { label: "Enter the Nursery", target: "nursery" }, { label: "Try the Master Bedroom", target: "master_bedroom_locked" } ] }, bathroom: { text: "The bathroom reeks of mildew and something sweet, like rotting fruit. The clawfoot bathtub is full to the brim with pitch-black water. Above the sink, the medicine cabinet mirror is completely shattered, missing large shards.", tension: 7, choices: [ { label: "Look into the tub", target: "tub_search" }, { label: "Look at the broken mirror", target: "mirror_look" }, { label: "Leave the bathroom", target: "upstairs_hallway" } ] }, mirror_look: { text: "You shine your light on the remaining shards of glass glued to the wall. You expect to see your exhausted, terrified face. \n\nInstead, the reflection of the hallway behind you is empty. You cannot see yourself in the glass. Just empty space where you should be standing.", tension: 8, choices: [ { label: "Step away", target: "bathroom" } ] }, tub_search: { text: "You approach the tub. The water is opaque, like liquid obsidian. You roll up your sleeve and plunge your arm in. It's ice cold. \n\nYour hand grazes the porcelain bottom, searching blindly. Suddenly, your fingers brush against... skin. Cold, dead skin. You flinch back, but force yourself to reach down again. \n\nNext to the submerged mass, you find a heavy brass key.", tension: 9, flags: { hasKey: true }, choices: [ { label: "Take the key and pull your arm out", target: "bathroom_after_key" } ] }, bathroom_after_key: { text: "You grip the brass key tightly. The black water ripples violently, as if whatever is beneath the surface is turning over. You stumble backward out of the room.", tension: 8, choices: [ { label: "Return to hallway", target: "upstairs_hallway" } ] }, nursery: { text: "You push open the door to the nursery. A mobile of wooden animals hangs over a white crib in the center of the room. The mobile is spinning rapidly, though there is no wind.\n\nThe walls are painted a soft pastel blue, ruined by frantic, charcoal scribbles stretching across every surface. Words repeated over and over.", tension: 7, choices: [ { label: "Read the scribbles", target: "nursery_walls" }, { label: "Look in the crib", target: "nursery_crib" }, { label: "Leave the nursery", target: "upstairs_hallway" } ] }, nursery_walls: { text: "You bring the flashlight close to the wall. The charcoal writing is jagged, erratic. \n\n'IT WON'T STOP CRYING'\n'MAKE IT STOP'\n'I JUST WANT TO SLEEP'\n'SILENCE AT LAST'\n\nThe handwriting... it looks exactly like yours.", tension: 8, choices: [ { label: "Back away from the wall", target: "nursery" } ] }, nursery_crib: { text: "You slowly step towards the crib. Your hands are shaking so badly the flashlight beam bounces erratically. You look over the railing.\n\nIt is empty. But the mattress is soaked through with a massive, dried rust-colored stain. Sitting in the center of the stain is a torn piece of paper.", tension: 8, choices: [ { label: "Read the paper", target: "nursery_note" } ] }, nursery_note: { text: "It's a hospital birth certificate. The name 'Lily' is crossed out with furious, tearing pen strokes. Beneath it, written in what looks like dried blood, is a single word:\n\n'GUILTY'.\n\nA sharp, agonizing pain spikes behind your eyes. A memory trying to force its way out.", tension: 9, choices: [ { label: "Leave the room immediately", target: "upstairs_hallway" } ] }, master_bedroom_locked: { text: "You grip the brass doorknob of the Master Bedroom. It refuses to turn. Locked. You press your ear against the heavy wood.\n\nFrom the other side, you hear heavy, wet breathing. And the faint, rhythmic sound of a knife being sharpened against a whetstone.", tension: 8, choices: [ { label: "Use the brass key", target: "master_bedroom_enter", condition: (flags) => flags.hasKey }, { label: "Walk away", target: "upstairs_hallway" } ] }, master_bedroom_enter: { text: "Your hands tremble as you slide the wet brass key into the lock. It turns with a heavy, sickening click. The whetstone sound stops instantly.\n\nYou push the door open. The hinges scream in the quiet house. You step over the threshold, raising your flashlight.", tension: 10, choices: [ { label: "Shine the light inside", target: "the_confrontation" } ] }, the_confrontation: { text: "The room is freezing. In the center, illuminated by the pale moonlight filtering through torn curtains, stands the figure you saw in the window. \n\nHe is tall, wearing clothes drenched in dark, dried fluid. He has his back to you. In his right hand, he holds a massive butcher knife.\n\n'Where is she?' you scream, your voice cracking with desperate rage. 'What did you do to Lily?!'", tension: 10, choices: [ { label: "Attack him", target: "the_charge" } ] }, the_charge: { text: "The figure doesn't move. He doesn't speak. \n\nA primal roar rips from your throat. You throw the flashlight, dropping your shoulder to tackle this monster, to tear him apart with your bare hands. You lunge forward with all your momentum, bracing for impact.\n\nCRASH.", tension: 10, choices: [ { label: "...", target: "the_twist_1" } ] }, the_twist_1: { text: "Shards of heavy glass rain down upon you, slicing into your skin. You stumble and fall hard to your knees, gasping for air.\n\nYou didn't hit a person. You hit a solid object. \n\nYou look up. The wooden frame of a large, antique floor mirror stands before you, its surface completely shattered by your impact.", tension: 10, choices: [ { label: "Look into the remaining shards", target: "the_twist_2" } ] }, the_twist_2: { text: "You stare into a large jagged shard still clinging to the frame. The tall monster is staring back at you.\n\nHe has your eyes. He has your face.\n\nYou look down at your hands. You aren't empty-handed. You never were. Your right hand is cramped, fingers locked in a death grip around the handle of the missing butcher knife. It is crusted in layers of black, dried blood.", tension: 10, choices: [ { label: "Look at your clothes", target: "the_twist_3" } ] }, the_twist_3: { text: "You look down at your own chest. The dark stains covering your shirt aren't from the broken glass. They are old. Days old.\n\nThe dam in your mind finally breaks. The suppressed memories flood your consciousness like freezing water. \n\nThe endless nights. The crying that wouldn't stop. The exhaustion that drove you insane. The moment something inside you snapped. The silence that followed. The terrible, permanent silence.", tension: 10, choices: [ { label: "Accept the truth", target: "the_end" } ] }, the_end: { text: "There is no monster haunting this house. There is no rescue mission. Lily isn't missing. She is exactly where you left her.\n\nThe flashlight on the floor flickers and dies, plunging the room into total darkness.\n\nThis isn't a search. This is your purgatory. And the loop is about to start again.", tension: 0, choices: [ { label: "WAKE UP", target: "wake", action: () => { window.location.reload() } } ] } }; // --- COMPONENTS --- // Typewriter effect component to slowly reveal text and build suspense const TypewriterText = ({ text, onComplete, speed = 40 }) => { const [displayedText, setDisplayedText] = useState(""); const [isTyping, setIsTyping] = useState(true); const containerRef = useRef(null); useEffect(() => { setDisplayedText(""); setIsTyping(true); let i = 0; // Quick typing for punctuation/spaces, slower for letters to mimic erratic thoughts const interval = setInterval(() => { if (i < text.length) { setDisplayedText((prev) => prev + text.charAt(i)); i++; // Auto-scroll to bottom of text if (containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } } else { clearInterval(interval); setIsTyping(false); if (onComplete) onComplete(); } }, speed); return () => clearInterval(interval); }, [text, speed, onComplete]); // Handle skip const handleSkip = () => { if (isTyping) { setDisplayedText(text); setIsTyping(false); if (onComplete) onComplete(); } }; return (
{displayedText} {isTyping && }
); }; export default function HorrorGame() { const [currentSceneId, setCurrentSceneId] = useState('title'); const [flags, setFlags] = useState({}); const [choicesVisible, setChoicesVisible] = useState(false); const [isGlitching, setIsGlitching] = useState(false); const scene = STORY[currentSceneId]; useEffect(() => { // Hide choices when scene changes until text is done typing setChoicesVisible(false); // Trigger audio tension changes audioEngine.setTension(scene.tension || 0); // Random visual glitch logic for high tension scenes if (scene.tension > 6) { const glitchInterval = setInterval(() => { if (Math.random() > 0.8) { setIsGlitching(true); setTimeout(() => setIsGlitching(false), 150); } }, 3000); return () => clearInterval(glitchInterval); } }, [currentSceneId, scene.tension]); const handleChoice = (choice) => { // Initialize audio on first user interaction if it's the title screen if (currentSceneId === 'title') { audioEngine.init(); } if (choice.action) { choice.action(); } if (choice.flags) { setFlags(prev => ({ ...prev, ...choice.flags })); } // Brief blackout transition setChoicesVisible(false); setTimeout(() => { setCurrentSceneId(choice.target); }, 500); // 500ms blackout between scenes }; // Filter choices based on conditions const availableChoices = scene.choices.filter( (choice) => !choice.condition || choice.condition(flags) ); return (
{/* CRT Scanline Overlay */}
{/* Vignette Overlay */}
{/* Title rendering vs Story rendering */} {currentSceneId === 'title' ? (

REMNANT

setChoicesVisible(true)} />
) : (
{/* The Text */} 8 ? 20 : 40} // Type faster in tense moments onComplete={() => setChoicesVisible(true)} />
)} {/* The Choices */}
{availableChoices.map((choice, index) => ( ))}
{/* Embedded CSS for custom scary effects */}