<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Metal Haunt</title>
<!-- Use a custom font for a better aesthetic -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Creepster&family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<!-- Tailwind CSS CDN for styling -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Apply smooth transition to the background color */
body {
transition: background-color 1s ease-in-out;
background-color: #0b0c10;
font-family: 'Roboto', sans-serif;
color: #c5c6c7;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
/* Set the custom Creepster font for headings and titles */
.title-font {
font-family: 'Creepster', cursive;
}
/* Custom styles for a metallic-monster aesthetic */
.game-card {
background: rgba(19, 21, 26, 0.95);
border: 2px solid #66fcf1;
box-shadow: 0 0 20px rgba(102, 252, 241, 0.5);
}
.button-choice {
background-color: #45a29e;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
border: 1px solid #66fcf1;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
}
.button-choice:hover {
background-color: #66fcf1;
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.5);
color: #0b0c10;
}
.monster-image {
animation: pulse 2s infinite ease-in-out;
border: 3px solid #66fcf1;
}
@keyframes pulse {
0% { transform: scale(1); box-shadow: 0 0 10px #66fcf1; }
50% { transform: scale(1.05); box-shadow: 0 0 20px #66fcf1, 0 0 30px #45a29e; }
100% { transform: scale(1); box-shadow: 0 0 10px #66fcf1; }
}
.fade-in {
animation: fadeIn 1s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.qte-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 1rem;
border-radius: 0.5rem;
background-color: rgba(30, 30, 40, 0.9);
box-shadow: inset 0 0 10px #66fcf1;
}
#qte-timer-bar {
height: 1rem;
width: 100%;
background-color: #0b0c10;
border: 2px solid #66fcf1;
border-radius: 9999px;
overflow: hidden;
}
#qte-progress {
height: 100%;
background-color: #45a29e;
transition: width 0.1s linear;
}
#qte-key {
font-family: 'Creepster', cursive;
font-size: 3rem;
animation: pulse-key 1s infinite;
}
@keyframes pulse-key {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); text-shadow: 0 0 10px #66fcf1; }
}
.bond-indicator {
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: bold;
white-space: nowrap;
}
.bond-worst { background-color: #ef4444; color: white; }
.bond-bad { background-color: #f97316; color: white; }
.bond-neutral { background-color: #eab308; color: black; }
.bond-good { background-color: #22c55e; color: white; }
.bond-best { background-color: #0ea5e9; color: white; }
</style>
</head>
<body class="bg-gray-900">
<div id="game-container" class="game-card max-w-2xl w-11/12 p-8 rounded-lg text-center shadow-lg mx-auto my-auto">
<h1 id="game-title" class="title-font text-5xl md:text-6xl text-center text-[#66fcf1] mb-6">The Metal Haunt</h1>
<img id="monster-img" class="monster-image w-full max-h-60 object-contain rounded-lg mb-6 fade-in"
src="https://placehold.co/600x250/212121/c5c6c7?text=The+Metallic+Monster"
onerror="this.src='https://placehold.co/600x250/212121/c5c6c7?text=The+Metallic+Monster';"
alt="Metallic Monster">
<div id="game-status" class="mb-6 flex flex-col sm:flex-row justify-between items-center text-sm md:text-base gap-2">
<p id="survivor-count" class="font-bold text-lg text-emerald-400">Survivors: 8</p>
<p id="choices-made" class="font-bold text-lg text-yellow-400">Choice: 1 / 20</p>
</div>
<div id="survivors-list" class="flex flex-wrap justify-center gap-2 mb-4 text-sm md:text-base"></div>
<div id="bonds-list" class="flex flex-wrap justify-center gap-2 mb-6 text-sm md:text-base"></div>
<div id="game-story" class="mb-8 p-4 bg-gray-800 rounded-lg text-left fade-in">
<p id="story-text" class="text-lg md:text-xl leading-relaxed"></p>
</div>
<div id="choices-container" class="flex flex-col gap-4"></div>
<!-- Quick Time Event UI -->
<div id="qte-ui" class="hidden qte-container">
<p class="text-2xl text-red-400 font-bold">Quick Time Event!</p>
<p id="qte-instruction" class="text-xl font-semibold"></p>
<div id="qte-timer-bar">
<div id="qte-progress"></div>
</div>
<p class="text-xl">Press <span id="qte-key" class="text-[#66fcf1] font-bold"></span></p>
</div>
<div id="game-message" class="hidden mt-6 text-xl md:text-2xl font-bold text-red-500 fade-in"></div>
<button id="restart-button" class="hidden mt-8 w-full py-3 rounded-lg font-bold text-xl md:text-2xl bg-teal-500 text-white hover:bg-teal-600 transition-colors duration-300">
Restart
</button>
</div>
<script>
// --- Game State Variables ---
let survivors = [];
let wrongChoices = 0;
let currentChoice = 0;
let gameOver = false;
let qteActive = false;
let qteTimeout;
let qteInterval;
let isSuperSecretEnding = false;
let qteProgress = 0;
const qteMaxProgress = 100;
// --- DOM Elements ---
const body = document.body;
const storyTextEl = document.getElementById('story-text');
const choicesContainerEl = document.getElementById('choices-container');
const survivorListEl = document.getElementById('survivors-list');
const bondsListEl = document.getElementById('bonds-list');
const survivorCountEl = document.getElementById('survivor-count');
const choicesMadeEl = document.getElementById('choices-made');
const gameMessageEl = document.getElementById('game-message');
const restartButtonEl = document.getElementById('restart-button');
const monsterImgEl = document.getElementById('monster-img');
const qteUIEl = document.getElementById('qte-ui');
const qteTimerBarEl = document.getElementById('qte-timer-bar');
const qteProgressEl = document.getElementById('qte-progress');
const qteKeyEl = document.getElementById('qte-key');
const qteInstructionEl = document.getElementById('qte-instruction');
// Define a list of bond levels for sorting and progression
const bondLevels = ["worst", "bad", "neutral", "good", "best"];
// --- Helper Function to get survivor status and bond level ---
const getSurvivorStatus = (name) => {
const survivor = survivors.find(s => s.name === name);
return survivor ? survivor.status : null;
};
const getBondLevel = (name1, name2) => {
const survivor1 = survivors.find(s => s.name === name1);
if (!survivor1 || !survivor1.bonds) return "neutral";
return survivor1.bonds[name2] || "neutral";
};
const bondDescriptions = {
"worst": "A hostile bond. They actively dislike each other and have no trust.",
"bad": "A strained bond. They disagree frequently and are on bad terms.",
"neutral": "A neutral bond. They don't know each other well or have no strong feelings.",
"good": "A positive bond. They trust and generally agree with each other.",
"best": "An unbreakable bond. They are close and will work together no matter what."
};
// Extended game events for the super secret ending
const extendedEvents = [
{
story: (survivors) => {
const alexAlive = getSurvivorStatus("Alex") === "alive";
const jakeAlive = getSurvivorStatus("Jake") === "alive";
const mariaAlive = getSurvivorStatus("Maria") === "alive";
let storyText = "The military truck is shredded as the monster, somehow alive, falls from the sky and lands on it. It seems stronger now. We need a plan. What do I do?";
if (alexAlive) storyText += ` Alex suggests fighting back using the destroyed truck as cover.`;
if (jakeAlive) storyText += ` Jake wants to run away, but it seems cowardly.`;
if (mariaAlive) storyText += ` Maria is already preparing to engage it.`;
return storyText;
},
choices: ["Run away in a different direction.", "Fight back using the destroyed truck as cover."],
correctChoice: "Fight back using the destroyed truck as cover.",
wrongConsequence: (survivors) => {
const alexAlive = getSurvivorStatus("Alex") === "alive";
if (alexAlive) return "We run, but the monster's optical sensors track us instantly. A high-velocity dart strikes Alex in the back of the head. He's dead before he hits the ground. We lost Alex.";
else return "We run, but the monster's optical sensors track us instantly. It's a chase to the death. You managed to escape, but at the cost of a friend. You are now being chased.";
},
victimName: "Alex"
},
{
story: (survivors) => {
const mariaAlive = getSurvivorStatus("Maria") === "alive";
const jakeAlive = getSurvivorStatus("Jake") === "alive";
let storyText = "Using the truck as a shield, we see a fuel tank nearby. The monster is trying to flank us. What do I do?";
if (mariaAlive) storyText += ` Maria suggests we sneak past the monster.`;
if (jakeAlive) storyText += ` Jake wants to throw a Molotov cocktail at the fuel tank.`;
return storyText;
},
choices: ["Throw a Molotov cocktail at the fuel tank.", "Try to sneak past the monster."],
correctChoice: "Throw a Molotov cocktail at the fuel tank.",
wrongConsequence: (survivors) => {
const mariaAlive = getSurvivorStatus("Maria") === "alive";
if (mariaAlive) return "We try to sneak past, but the monster catches up. Its massive claws tear Maria apart, leaving nothing but a bloody mess. We lost Maria.";
else return "We try to sneak past, but the monster catches up. A jagged piece of metal pierces through a friend's chest, they are dead before they hit the ground. You lost a friend.";
},
victimName: "Maria"
},
{
story: (survivors) => {
const jakeAlive = getSurvivorStatus("Jake") === "alive";
let storyText = "The monster is engulfed in flames. It's blinded and in agony. Now is our chance! What do I do?";
if (jakeAlive) storyText += ` Jake points to a heavy pipe nearby.`;
return storyText;
},
qte: {
type: "tap",
duration: 3000,
key: " "
},
correctChoice: "Press space to hit it with a heavy pipe.",
wrongConsequence: (survivors) => {
const jakeAlive = getSurvivorStatus("Jake") === "alive";
if (jakeAlive) return "You hesitate. The monster, despite being on fire, turns and impales Jake with a jagged piece of rebar. He screams in agony before his life fades away. We lost Jake.";
else return "You hesitate. The monster, despite being on fire, turns and slashes a friend with a jagged piece of rebar. They bleed out quickly. You lost a friend.";
},
victimName: "Jake"
},
{
story: (survivors) => {
const alexAlive = getSurvivorStatus("Alex") === "alive";
const jakeAlive = getSurvivorStatus("Jake") === "alive";
let storyText = "The monster's leg is crippled, but it's still coming. I see an unstable piece of factory equipment. What do I do?";
if (alexAlive) storyText += ` Alex suggests we use the forklift to topple it onto the monster.`;
if (jakeAlive) storyText += ` Jake wants to try and find a way to trap the monster.`;
return storyText;
},
choices: ["Use the forklift to topple it onto the monster.", "Try to find a way to trap the monster."],
correctChoice: "Use the forklift to topple it onto the monster.",
wrongConsequence: (survivors) => {
const alexAlive = getSurvivorStatus("Alex") === "alive";
if (alexAlive) return "We try to trap it, but it's too fast. Alex is caught and has his skull crushed in the monster's hand. We lost Alex.";
else return "We try to trap it, but it's too fast. A friend is caught and has their skull crushed in the monster's hand. You lost a friend.";
},
victimName: "Alex"
},
{
story: (survivors) => {
const mariaAlive = getSurvivorStatus("Maria") === "alive";
let storyText = "The equipment falls on the monster, pinning it to the ground. It's struggling to get free. What do I do?";
if (mariaAlive) storyText += ` Maria shouts for you to finish it off while you have the chance.`;
return storyText;
},
qte: {
type: "tap",
duration: 2000,
key: "e"
},
correctChoice: "Press 'E' to finish it off.",
wrongConsequence: (survivors) => {
const mariaAlive = getSurvivorStatus("Maria") === "alive";
if (mariaAlive) return "You freeze up. The monster breaks free and turns on you, impaling Maria on its claw and throwing her into a pile of rubble. We lost Maria.";
else return "You freeze up. The monster breaks free and turns on you, impaling a friend on its claw and throwing them into a pile of rubble. You lost a friend.";
},
victimName: "Maria"
},
{
story: () => "The monster is severely damaged but not dead. It lets out a final, terrifying shriek before it attempts to escape. What do I do?",
choices: ["Go after it.", "Let it go and escape."],
correctChoice: "Go after it.",
wrongConsequence: () => "We let it go, a mistake. It escapes and will live to torment more people. You survived, but at a cost. Your final moments are filled with guilt and regret.",
victimName: "Amie"
},
{
story: (survivors) => {
const jakeAlive = getSurvivorStatus("Jake") === "alive";
let storyText = "We chase it to a control room. It has one last weapon, a sonic blast. We need to disable it fast. What do I do?";
if (jakeAlive) storyText += ` Jake points to the power cables and shouts to cut them.`;
return storyText;
},
qte: {
type: "tap",
duration: 3000,
key: "d"
},
correctChoice: "Press 'D' to cut the power cables.",
wrongConsequence: (survivors) => {
const jakeAlive = getSurvivorStatus("Jake") === "alive";
if (jakeAlive) return "You are too slow. The monster releases a sonic blast. Jake's internal organs rupture, and he collapses, bleeding from his mouth and ears. We lost Jake.";
else return "You are too slow. The monster releases a sonic blast. A friend's internal organs rupture, and they collapse, bleeding from their mouth and ears. You lost a friend.";
},
victimName: "Jake"
},
{
story: (survivors) => {
const alexAlive = getSurvivorStatus("Alex") === "alive";
let storyText = "The monster is weakened. It's cornered. The entire factory is about to collapse. The only way out is through the exit tunnel. What do I do?";
if (alexAlive) storyText += ` Alex suggests luring the monster towards the tunnel and escaping.`;
return storyText;
},
choices: ["Lure the monster towards the tunnel and escape.", "Let it get crushed in the collapsing factory."],
correctChoice: "Lure the monster towards the tunnel and escape.",
wrongConsequence: (survivors) => {
const alexAlive = getSurvivorStatus("Alex") === "alive";
if (alexAlive) return "We let the monster get crushed, but a falling support beam pins Alex's leg. He's trapped. The monster, with its final burst of strength, crawls towards him and tears him limb from limb. We lost Alex.";
else return "We let the monster get crushed, but a falling support beam pins a friend's leg. They are trapped. The monster, with its final burst of strength, crawls towards them and tears them limb from limb. You lost a friend.";
},
victimName: "Alex"
},
{
story: (survivors) => {
const mariaAlive = getSurvivorStatus("Maria") === "alive";
let storyText = "The monster is at the entrance of the tunnel, but the door is closing. The only way to stop it is to get close and disengage the locking mechanism. What do I do?";
if (mariaAlive) storyText += ` Maria shouts to disengage the lock.`;
return storyText;
},
qte: {
type: "tap",
duration: 3000,
key: "f"
},
correctChoice: "Press 'F' to disengage the lock.",
wrongConsequence: (survivors) => {
const mariaAlive = getSurvivorStatus("Maria") === "alive";
if (mariaAlive) return "You miss. The door crushes Maria's head, and the monster crawls inside and eats the rest of her. We lost Maria.";
else return "You miss. The door crushes a friend's head, and the monster crawls inside and eats the rest of them. You lost a friend.";
},
victimName: "Maria"
},
{
story: () => "The monster is finally dead. We are at the end of the tunnel. We made it. It's over. What do I do?",
choices: ["You have survived!", "Replay?"],
correctChoice: "You have survived!",
wrongConsequence: () => "You decide to replay the game, but the monster has returned to haunt you. A metal hand pierces your chest. You die. Amie is gone.",
victimName: "Amie"
}
];
// Combine base and extended events
const gameEvents = [
{
story: (survivors) => {
const benAlive = getSurvivorStatus("Ben") === "alive";
const sarahAlive = getSurvivorStatus("Sarah") === "alive";
const amieAlive = getSurvivorStatus("Amie") === "alive";
const alexAlive = getSurvivorStatus("Alex") === "alive";
const jakeAlive = getSurvivorStatus("Jake") === "alive";
let storyText = "The rusted door groans shut behind us, trapping us inside the abandoned factory. A rhythmic clanking echoes from the darkness. ";
if (amieAlive) {
storyText += `Amie, as the leader, needs to make a decision.`;
}
if (jakeAlive && alexAlive && getBondLevel("Jake", "Alex") === "worst") {
storyText += ` Jake wants to hide, but Alex thinks it's a cowardly move.`;
} else if (benAlive && sarahAlive && getBondLevel("Ben", "Sarah") === "bad") {
storyText += ` Ben suggests splitting up, but Sarah argues that's a terrible idea.`;
}
return storyText + " What do I do?";
},
choices: ["Hide behind the machine press.", "Crawl into the air vent."],
correctChoice: "Crawl into the air vent.",
wrongConsequence: (survivors) => {
const benAlive = getSurvivorStatus("Ben") === "alive";
if (benAlive) return "I try to hide, but the monster's hydraulic claws tear through the machine press, crushing Ben instantly. His screams are silenced in a sickening crunch. We just lost Ben.";
else return "I try to hide, but the monster's hydraulic claws tear through the machine press, crushing a friend instantly. Their screams are silenced in a sickening crunch. You just lost a friend.";
},
victimName: "Ben"
},
{
story: (survivors) => {
const sarahAlive = getSurvivorStatus("Sarah") === "alive";
const alexAlive = getSurvivorStatus("Alex") === "alive";
const amieAlive = getSurvivorStatus("Amie") === "alive";
let storyText = "The metallic scraping is now just outside the vent. It sounds like a hundred knives on a chalkboard. ";
if (sarahAlive && getBondLevel("Amie", "Sarah") === "best") {
storyText += "Sarah is starting to panic, clinging to me for support. ";
} else if (alexAlive) {
storyText += "Alex is calmly looking for an escape, trying to reassure the group. ";
} else {
storyText += "We're all holding our breath, waiting for a sign. ";
}
return storyText + "What do I do?";
},
choices: ["Wait for it to pass.", "Make a break for the exit door."],
correctChoice: "Wait for it to pass.",
wrongConsequence: (survivors) => {
const sarahAlive = getSurvivorStatus("Sarah") === "alive";
if (sarahAlive) return "We make a run for it. The monster hears the rustling and yanks Sarah out of the vent. She is impaled on its jagged appendages, her body a grotesque ornament. Sarah is gone.";
else return "We make a run for it. The monster hears the rustling and yanks a friend out of the vent. They are impaled on its jagged appendages, their body a grotesque ornament. You lost a friend.";
},
victimName: "Sarah"
},
{
story: (survivors) => {
const chloeAlive = getSurvivorStatus("Chloe") === "alive";
const leoAlive = getSurvivorStatus("Leo") === "alive";
const mariaAlive = getSurvivorStatus("Maria") === "alive";
let storyText = "It's gone. The coast is clear. We find a security terminal. ";
if (chloeAlive && leoAlive && getBondLevel("Chloe", "Leo") === "good") {
storyText += "Chloe wants to lock the factory doors, while Leo, with a nod of agreement, thinks they should search for a map first. They're a good team. ";
} else if (mariaAlive) {
storyText += "Maria, ever the pragmatist, suggests we find a map before we make any rash decisions. ";
}
return storyText + "What do I do?";
},
choices: ["Search for a map.", "Lock the factory doors."],
correctChoice: "Search for a map.",
wrongConsequence: (survivors) => {
const leoAlive = getSurvivorStatus("Leo") === "alive";
if (leoAlive) return "We decide to lock the doors. As we do, the monster's piston-like arm smashes through the glass, severing Leo's arm before he can pull away. He bleeds out quickly. We just lost Leo.";
else return "We decide to lock the doors. As we do, the monster's piston-like arm smashes through the glass, severing a friend's arm before they can pull away. They bleed out quickly. You just lost a friend.";
},
victimName: "Leo"
},
{
story: (survivors) => {
const chloeAlive = getSurvivorStatus("Chloe") === "alive";
return `The map shows a maintenance tunnel. It's the only way out. We need a keycard, but it's in the office. ${chloeAlive ? "Chloe wants to split up, but I think that's too risky. " : "We all agree splitting up is a bad idea. "} What do I do?`;
},
choices: ["Split up to find the keycard.", "Stick together and search the office."],
correctChoice: "Stick together and search the office.",
wrongConsequence: (survivors) => {
const chloeAlive = getSurvivorStatus("Chloe") === "alive";
if (chloeAlive) return "We split up, and it's a mistake. The monster ambushes Chloe in the hallway, its metallic mandibles tearing her in half. Chloe is gone.";
else return "We split up, and it's a mistake. The monster ambushes a friend in the hallway, its metallic mandibles tearing them in half. You lost a friend.";
},
victimName: "Chloe"
},
{
story: (survivors) => {
const benAlive = getSurvivorStatus("Ben") === "alive";
const sarahAlive = getSurvivorStatus("Sarah") === "alive";
const jakeAlive = getSurvivorStatus("Jake") === "alive";
const mariaAlive = getSurvivorStatus("Maria") === "alive";
let storyText = "We found the keycard, but the monster is blocking the main hallway. ";
if (benAlive && sarahAlive && getBondLevel("Ben", "Sarah") === "worst") {
storyText += "Ben suggests we use a forklift to distract it, but Sarah says we should try to sneak past it, throwing a glare at Ben. ";
} else if (jakeAlive && mariaAlive && getBondLevel("Jake", "Maria") === "best") {
storyText += "Jake and Maria, acting in sync, suggest a coordinated distraction. They'll use the forklift while we sneak past. ";
}
return storyText + "What do I do?";
},
choices: ["Use a forklift to distract it.", "Sneak past it while it's scanning."],
correctChoice: "Use a forklift to distract it.",
wrongConsequence: (survivors) => {
const sarahAlive = getSurvivorStatus("Sarah") === "alive";
if (sarahAlive) return "We try to sneak past it, but the monster's sensors detect our movement. It hurls a heavy gear that smashes Sarah's head, leaving nothing but a bloody, broken mess. We just lost Sarah.";
else return "We try to sneak past it, but the monster's sensors detect our movement. It hurls a heavy gear that smashes a friend's head, leaving nothing but a bloody, broken mess. You just lost a friend.";
},
victimName: "Sarah"
},
{
story: () => "The monster is distracted by the forklift. We can reach the tunnel. Suddenly, it turns its head and we hear a mechanical whirring sound. I have to react instantly! What do I do?",
qte: {
type: "tap",
duration: 3000,
key: " "
},
correctChoice: "Press space to run.",
wrongConsequence: (survivors) => {
const benAlive = getSurvivorStatus("Ben") === "alive";
if (benAlive) return "I freeze for a second, and the monster's optical sensor locks onto us. A high-velocity dart shoots out, striking Ben in the neck and paralyzing him. The monster takes its time with him. Ben is dead.";
else return "I freeze for a second, and the monster's optical sensor locks onto us. A high-velocity dart shoots out, striking a friend in the neck and paralyzing them. The monster takes its time with them. You lost a friend.";
},
victimName: "Ben"
},
{
story: (survivors) => {
const sarahAlive = getSurvivorStatus("Sarah") === "alive";
const leoAlive = getSurvivorStatus("Leo") === "alive";
const alexAlive = getSurvivorStatus("Alex") === "alive";
const amieAlive = getSurvivorStatus("Amie") === "alive";
let storyText = "We enter the maintenance tunnel. It's dark and narrow, filled with pipes. The clanking sound is behind us. ";
if (sarahAlive && leoAlive && getBondLevel("Sarah", "Leo") === "bad") {
storyText += "Sarah thinks we should follow the tunnel straight ahead, but Leo thinks we should climb the pipes, openly arguing about the best way. ";
} else if (alexAlive && amieAlive && getBondLevel("Alex", "Amie") === "good") {
storyText += "Alex and I agree that climbing the pipes is the best bet, a silent understanding passing between us. ";
} else {
storyText += "We need to make a quick decision. ";
}
return storyText + "What do I do?";
},
choices: ["Climb through the pipe network.", "Follow the tunnel straight ahead."],
correctChoice: "Climb through the pipe network.",
wrongConsequence: (survivors) => {
const mariaAlive = getSurvivorStatus("Maria") === "alive";
if (mariaAlive) return "Following the tunnel leads to a dead end. The monster's metal foot crunches on Maria's skull as it catches up to us. Maria is gone.";
else return "Following the tunnel leads to a dead end. The monster's metal foot crunches on a friend's skull as it catches up to us. You lost a friend.";
},
victimName: "Maria"
},
{
story: (survivors) => {
const leoAlive = getSurvivorStatus("Leo") === "alive";
let storyText = "The pipe network leads to a huge turbine room. The monster enters the tunnel and sees us. I have to act fast! What do I do?";
if (leoAlive) storyText += ` Leo screams to activate the emergency shutdown switch.`;
return storyText;
},
qte: {
type: "tap",
duration: 3000,
key: " "
},
correctChoice: "Press space to activate the emergency shutdown switch.",
wrongConsequence: (survivors) => {
const leoAlive = getSurvivorStatus("Leo") === "alive";
if (leoAlive) return "I hesitate, and Leo is grabbed by the monster. He's held up and his spinal column is forcefully removed. We just lost Leo.";
else return "I hesitate, and a friend is grabbed by the monster. They are held up and their spinal column is forcefully removed. You just lost a friend.";
},
victimName: "Leo"
},
{
story: (survivors) => {
const chloeAlive = getSurvivorStatus("Chloe") === "alive";
const benAlive = getSurvivorStatus("Ben") === "alive";
const jakeAlive = getSurvivorStatus("Jake") === "alive";
const aliveSurvivors = survivors.filter(s => s.status === "alive");
let storyText = "The turbine blades spin to a halt, but the monster is unharmed. We see a control panel with a single button. ";
if (chloeAlive && benAlive) {
if (getBondLevel("Chloe", "Ben") === "good") {
storyText += "Chloe wants to press the button, and Ben immediately backs her up. They seem to trust each other's opinions. ";
} else {
storyText += "Chloe wants to press the button, but Ben thinks we should run and find another hiding spot. They look at me for the final decision. ";
}
} else if (jakeAlive) {
storyText += "Jake quickly points out the button and suggests we press it. He's thinking on his feet. ";
} else if (aliveSurvivors.length > 1) {
storyText += "There is a control panel with a single button. ";
} else {
storyText += "I see a control panel with a single button.";
}
return storyText + "What do I do?";
},
choices: ["Press the button and see what happens.", "Run and find another hiding spot."],
correctChoice: "Press the button and see what happens.",
wrongConsequence: (survivors) => {
const chloeAlive = getSurvivorStatus("Chloe") === "alive";
if (chloeAlive) return "We decide to run, but the monster catches up. Chloe's arm is twisted off and used to beat her to death. We lost Chloe.";
else return "We decide to run, but the monster catches up. A friend's arm is twisted off and used to beat them to death. You lost a friend.";
},
victimName: "Chloe"
},
{
story: (survivors) => {
const benAlive = getSurvivorStatus("Ben") === "alive";
const sarahAlive = getSurvivorStatus("Sarah") === "alive";
const jakeAlive = getSurvivorStatus("Jake") === "alive";
const mariaAlive = getSurvivorStatus("Maria") === "alive";
let storyText = "A trap door opens beneath the monster, and it falls into a pit of molten metal. We hear a final, deafening screech. We're safe. For now... ";
if (benAlive && sarahAlive && getBondLevel("Ben", "Sarah") === "worst") {
storyText += "Ben wants to climb down to check on it, but Sarah says we should find the exit, giving Ben a look of utter contempt. ";
} else if (jakeAlive && mariaAlive && getBondLevel("Jake", "Maria") === "best") {
storyText += "Maria says we need to find the exit and Jake agrees immediately. ";
} else {
storyText += "We need to find the exit. ";
}
return storyText + "What do I do?";
},
choices: ["Climb down into the pit to see if it's dead.", "Find the exit."],
correctChoice: "Find the exit.",
wrongConsequence: (survivors) => {
const benAlive = getSurvivorStatus("Ben") === "alive";
if (benAlive) return "I foolishly go down into the pit to make sure it's dead, but it's a trap. A metallic hand grabs Ben from behind, pulling him down into the molten metal. He melts slowly. We lost Ben.";
else return "I foolishly go down into the pit to make sure it's dead, but it's a trap. A metallic hand grabs a friend from behind, pulling them down into the molten metal. They melt slowly. You lost a friend.";
},
victimName: "Ben"
},
{
story: (survivors) => {
const leoAlive = getSurvivorStatus("Leo") === "alive";
const sarahAlive = getSurvivorStatus("Sarah") === "alive";
const alexAlive = getSurvivorStatus("Alex") === "alive";
let storyText = "The exit is a rusted iron gate. It's locked. We have to find a way to open it. ";
if (leoAlive && sarahAlive && getBondLevel("Leo", "Sarah") === "bad") {
storyText += "Leo wants to look for a key, but Sarah thinks we should try to force it open with a metal bar. Their lack of trust is obvious. ";
} else if (alexAlive && leoAlive && getBondLevel("Alex", "Leo") === "good") {
storyText += "Alex and Leo agree that forcing the gate open is the best option, their teamwork is on full display. ";
}
return storyText + "What do I do?";
},
choices: ["Look for a key.", "Try to force it open with a metal bar."],
correctChoice: "Try to force it open with a metal bar.",
wrongConsequence: (survivors) => {
const leoAlive = getSurvivorStatus("Leo") === "alive";
if (leoAlive) return "Leo spends too long looking for a key, and the monster returns. He tries to run, but his legs are crushed by the monster's mechanical feet. He is then eaten alive. We lost Leo.";
else return "A friend spends too long looking for a key, and the monster returns. They try to run, but their legs are crushed by the monster's mechanical feet. They are then eaten alive. You lost a friend.";
},
victimName: "Leo"
},
{
story: (survivors) => {
const sarahAlive = getSurvivorStatus("Sarah") === "alive";
let storyText = "The gate is slowly creaking open. Suddenly, the monster appears again, limping, but still a threat. I have to act fast! What do I do?";
if (sarahAlive) storyText += ` Sarah screams to run.`;
return storyText;
},
qte: {
type: "tap",
duration: 2000,
key: " "
},
correctChoice: "Press space to run.",
wrongConsequence: (survivors) => {
const sarahAlive = getSurvivorStatus("Sarah") === "alive";
if (sarahAlive) return "I freeze. The monster is not phased by it. Sarah is caught by the monster and is brutally ripped in half. Sarah is gone.";
else return "I freeze. The monster is not phased by it. A friend is caught by the monster and is brutally ripped in half. You lost a friend.";
},
victimName: "Sarah"
},
{
story: (survivors) => {
const benAlive = getSurvivorStatus("Ben") === "alive";
let storyText = "We escape the factory, but we're now in a dark forest. I hear the monster's clanking getting closer. What do I do?";
if (benAlive) storyText += ` Ben suggests climbing a tree.`;
return storyText;
},
choices: ["Climb a tree.", "Hide in a hollow log."],
correctChoice: "Hide in a hollow log.",
wrongConsequence: (survivors) => {
const benAlive = getSurvivorStatus("Ben") === "alive";
if (benAlive) return "Ben climbs a tree. The monster looks up and sees him. It uses a grappling hook and yanks Ben out of the tree. He falls and breaks his neck. We lost Ben.";
else return "A friend climbs a tree. The monster looks up and sees them. It uses a grappling hook and yanks them out of the tree. They fall and break their neck. You lost a friend.";
},
victimName: "Ben"
},
{
story: (survivors) => {
const chloeAlive = getSurvivorStatus("Chloe") === "alive";
let storyText = "I see an old cabin ahead. The monster is right behind us. What do I do?";
if (chloeAlive) storyText += ` Chloe says we should run inside and barricade the door.`;
return storyText;
},
choices: ["Run inside and barricade the door.", "Run off the path to confuse it."],
correctChoice: "Run inside and barricade the door.",
wrongConsequence: (survivors) => {
const chloeAlive = getSurvivorStatus("Chloe") === "alive";
if (chloeAlive) return "I run off the path to confuse it, but it's a mistake. The monster finds Chloe and crushes her head with its metallic hand. We lost Chloe.";
else return "I run off the path to confuse it, but it's a mistake. The monster finds a friend and crushes their head with its metallic hand. You lost a friend.";
},
victimName: "Chloe"
},
{
story: (survivors) => {
const leoAlive = getSurvivorStatus("Leo") === "alive";
const jakeAlive = getSurvivorStatus("Jake") === "alive";
let storyText = "We are inside the cabin. The monster is pounding on the door. I see a gas can. ";
if (leoAlive && jakeAlive && getBondLevel("Leo", "Jake") === "good") {
storyText += "Jake suggests using the gas can, and Leo immediately backs him up. They've found common ground. ";
} else if (leoAlive) {
storyText += "Leo wants to use the gas can. ";
}
return storyText + "What do I do?";
},
choices: ["Pour the gas on the floor and light a match.", "Try to find a weapon."],
correctChoice: "Pour the gas on the floor and light a match.",
wrongConsequence: (survivors) => {
const leoAlive = getSurvivorStatus("Leo") === "alive";
if (leoAlive) return "Leo decides to search for a weapon. He is too slow. The monster breaks in and slices his torso with its sharp claws. We just lost Leo.";
else return "A friend decides to search for a weapon. They are too slow. The monster breaks in and slices their torso with its sharp claws. You lost a friend.";
},
victimName: "Leo"
},
{
story: (survivors) => {
const chloeAlive = getSurvivorStatus("Chloe") === "alive";
const benAlive = getSurvivorStatus("Ben") === "alive";
const mariaAlive = getSurvivorStatus("Maria") === "alive";
let storyText = "The monster is set on fire and stumbles away, shrieking. The cabin is burning down. ";
if (chloeAlive && benAlive && getBondLevel("Chloe", "Ben") === "neutral") {
storyText += "Chloe wants to climb out the back window, but Ben thinks we should wait for the fire to pass. They look at me for the final decision. ";
} else if (mariaAlive) {
storyText += "Maria points to the back window and shouts that we need to escape now. ";
}
return storyText + "We need to get out now. What do I do?";
},
choices: ["Climb out the back window.", "Wait for the fire to pass."],
correctChoice: "Climb out the back window.",
wrongConsequence: (survivors) => {
const benAlive = getSurvivorStatus("Ben") === "alive";
if (benAlive) return "Ben waits. The monster returns and finds him, not as a target but as a new host. Ben's body is mutilated and merged with the monster. Ben is gone.";
else return "A friend waits. The monster returns and finds them, not as a target but as a new host. Their body is mutilated and merged with the monster. You lost a friend.";
},
victimName: "Ben"
},
{
story: (survivors) => {
const alexAlive = getSurvivorStatus("Alex") === "alive";
let storyText = "We are back outside. The monster is still burning but is charging at us. I see a cliff edge. ";
if (alexAlive && getBondLevel("Alex", "Amie") === "best") {
storyText += "Alex tells me to run, saying he will distract it. It is a sacrifice, but it may save us. ";
}
return storyText + "What do I do?";
},
choices: ["Jump and hope for the best.", "Lure the monster off the cliff."],
correctChoice: "Lure the monster off the cliff.",
wrongConsequence: (survivors) => {
const sarahAlive = getSurvivorStatus("Sarah") === "alive";
if (sarahAlive) return "Sarah jumps off the cliff. She lands on rocks and breaks her back. We just lost Sarah.";
else return "A friend jumps off the cliff. They land on rocks and break their back. You just lost a friend.";
},
victimName: "Sarah"
},
{
story: (survivors) => {
const chloeAlive = getSurvivorStatus("Chloe") === "alive";
const jakeAlive = getSurvivorStatus("Jake") === "alive";
let storyText = "The monster charges at us and goes off the cliff. It's gone for good. We're safe. For now... ";
if (chloeAlive && jakeAlive && getBondLevel("Chloe", "Jake") === "good") {
storyText += "Chloe says we should find the nearest town, and Jake quickly points out a road on the map we found. ";
} else if (chloeAlive) {
storyText += "Chloe says we should find the nearest town. ";
} else {
storyText += "We should find the nearest town. ";
}
return storyText + "What do I do?";
},
choices: ["Find the nearest town.", "Stay and rest."],
correctChoice: "Find the nearest town.",
wrongConsequence: (survivors) => {
const chloeAlive = getSurvivorStatus("Chloe") === "alive";
if (chloeAlive) return "Chloe rests. She is found by a group of cultists who worship the metallic monster and is taken captive. She is never heard from again. We lost Chloe.";
else return "A friend rests. They are found by a group of cultists who worship the metallic monster and is taken captive. They are never heard from again. You lost a friend.";
},
victimName: "Chloe"
},
{
story: (survivors) => {
const alexAlive = getSurvivorStatus("Alex") === "alive";
const mariaAlive = getSurvivorStatus("Maria") === "alive";
const leoAlive = getSurvivorStatus("Leo") === "alive";
let storyText = "We are on the road to town. We see a military truck. ";
if (alexAlive && mariaAlive && getBondLevel("Alex", "Maria") === "best") {
storyText += "Alex and Maria instantly agree to flag it down, their shared history and trust guiding them. ";
} else if (alexAlive) {
storyText += "Alex says we should flag it down. ";
} else if (leoAlive) {
storyText += "Leo says we should flag it down. ";
}
return storyText + "What do I do?";
},
choices: ["Flag it down.", "Hide and wait for it to pass."],
correctChoice: "Flag it down.",
wrongConsequence: (survivors) => {
const leoAlive = getSurvivorStatus("Leo") === "alive";
if (leoAlive) return "I hide. The truck passes, and the monster, which was hiding in the truck, catches Leo and uses his body as a puppet. We lost Leo.";
else return "I hide. The truck passes, and the monster, which was hiding in the truck, catches a friend and uses their body as a puppet. You lost a friend.";
},
victimName: "Leo"
},
{
story: (survivors) => {
const leoAlive = getSurvivorStatus("Leo") === "alive";
let storyText = "The soldiers save us. We are taken to a safe place. We survived. Congratulations!";
if (leoAlive) storyText += ` Leo is also with you.`;
return storyText;
},
choices: ["You have survived all 20 choices!", "Replay?"],
correctChoice: "You have survived all 20 choices!",
wrongConsequence: (survivors) => {
const leoAlive = getSurvivorStatus("Leo") === "alive";
if (leoAlive) return "I decide to replay the game, but I am too late. The monster's metal fingers pierce Leo's skull as I try to hit the restart button. We lost Leo.";
else return "I decide to replay the game, but I am too late. The monster's metal fingers pierce a friend's skull as I try to hit the restart button. You lost a friend.";
},
victimName: "Leo"
},
...extendedEvents
];
// --- Game Logic Functions ---
/**
* Initializes the game state and UI.
*/
function startGame() {
// Define all characters and their bonds at the start
survivors = [
{ name: "Amie", status: "alive", isProtagonist: true, bonds: { "Alex": "best", "Sarah": "good", "Ben": "worst", "Chloe": "neutral", "Leo": "neutral", "Jake": "good", "Maria": "good" } },
{ name: "Alex", status: "alive", isProtagonist: false, bonds: { "Amie": "best", "Sarah": "good", "Ben": "bad", "Chloe": "good", "Leo": "good", "Jake": "worst", "Maria": "best" } },
{ name: "Sarah", status: "alive", isProtagonist: false, bonds: { "Amie": "good", "Alex": "good", "Ben": "worst", "Chloe": "neutral", "Leo": "bad", "Jake": "bad", "Maria": "neutral" } },
{ name: "Ben", status: "alive", isProtagonist: false, bonds: { "Amie": "worst", "Alex": "bad", "Sarah": "worst", "Chloe": "good", "Leo": "bad", "Jake": "bad", "Maria": "worst" } },
{ name: "Chloe", status: "alive", isProtagonist: false, bonds: { "Amie": "neutral", "Alex": "good", "Sarah": "neutral", "Ben": "good", "Leo": "good", "Jake": "good", "Maria": "neutral" } },
{ name: "Leo", status: "alive", isProtagonist: false, bonds: { "Amie": "neutral", "Alex": "good", "Sarah": "bad", "Ben": "bad", "Chloe": "good", "Jake": "neutral", "Maria": "bad" } },
{ name: "Jake", status: "alive", isProtagonist: false, bonds: { "Amie": "good", "Alex": "worst", "Sarah": "bad", "Ben": "bad", "Chloe": "good", "Leo": "neutral", "Maria": "best" } },
{ name: "Maria", status: "alive", isProtagonist: false, bonds: { "Amie": "good", "Alex": "best", "Sarah": "neutral", "Ben": "worst", "Chloe": "neutral", "Leo": "bad", "Jake": "best" } },
];
// Reset state variables
wrongChoices = 0;
currentChoice = 0;
gameOver = false;
isSuperSecretEnding = false;
// Reset UI elements
body.style.backgroundColor = '#0b0c10';
gameMessageEl.textContent = '';
gameMessageEl.classList.add('hidden');
restartButtonEl.classList.add('hidden');
qteUIEl.classList.add('hidden');
// Render initial game state
renderGame();
}
/**
* Renders the current game state to the UI.
*/
function renderGame() {
// Clear previous choices and story
choicesContainerEl.innerHTML = '';
storyTextEl.textContent = '';
survivorListEl.innerHTML = '';
bondsListEl.innerHTML = '';
// Sort survivors by bond level with the protagonist
const protagonist = survivors.find(s => s.isProtagonist);
const otherSurvivors = survivors.filter(s => !s.isProtagonist);
otherSurvivors.sort((a, b) => {
// Deceased and Injured at the end
if (a.status === 'deceased' && b.status !== 'deceased') return 1;
if (a.status !== 'deceased' && b.status === 'deceased') return -1;
if (a.status === 'injured' && b.status !== 'injured') return 1;
if (a.status !== 'injured' && b.status === 'injured') return -1;
const aBondIndex = bondLevels.indexOf(protagonist.bonds[a.name] || 'neutral');
const bBondIndex = bondLevels.indexOf(protagonist.bonds[b.name] || 'neutral');
return bBondIndex - aBondIndex;
});
const sortedSurvivors = [protagonist, ...otherSurvivors];
// Update status indicators
survivorCountEl.textContent = `Survivors: ${survivors.filter(s => s.status === 'alive' || s.status === 'injured').length}`;
choicesMadeEl.textContent = `Choice: ${currentChoice + 1} / ${gameEvents.length}`;
// Display survivor list with status indicators
sortedSurvivors.forEach(survivor => {
const survivorEl = document.createElement('span');
survivorEl.textContent = survivor.name;
survivorEl.classList.add('px-3', 'py-1', 'rounded-full', 'font-bold', 'text-sm');
if (survivor.status === 'alive') {
survivorEl.classList.add('bg-green-500', 'text-white');
} else if (survivor.status === 'injured') {
survivorEl.classList.add('bg-orange-500', 'text-white');
} else {
survivorEl.classList.add('bg-red-500', 'text-white', 'line-through');
}
survivorListEl.appendChild(survivorEl);
});
// Display bonds with the protagonist
if (protagonist) {
otherSurvivors.forEach(otherSurvivor => {
const bondLevel = getBondLevel(protagonist.name, otherSurvivor.name);
const bondEl = document.createElement('div');
bondEl.classList.add('flex', 'items-center', 'gap-1');
const nameSpan = document.createElement('span');
nameSpan.textContent = otherSurvivor.name + ": ";
nameSpan.classList.add('text-base', 'font-semibold');
bondEl.appendChild(nameSpan);
const bondSpan = document.createElement('span');
bondSpan.textContent = bondLevel.toUpperCase();
bondSpan.classList.add('bond-indicator', `bond-${bondLevel}`);
bondSpan.setAttribute('title', bondDescriptions[bondLevel]);
bondEl.appendChild(bondSpan);
bondsListEl.appendChild(bondEl);
});
}
// Display current story text, dynamically generated based on survivor status and bonds
const event = gameEvents[currentChoice];
if (typeof event.story === 'function') {
storyTextEl.textContent = event.story(survivors);
} else {
storyTextEl.textContent = event.story;
}
// Check if current event has a QTE
if (event.qte) {
choicesContainerEl.classList.add('hidden');
startQTE(event.qte);
} else {
choicesContainerEl.classList.remove('hidden');
// Display choices as buttons
event.choices.forEach(choice => {
const button = document.createElement('button');
button.textContent = choice;
button.classList.add('button-choice', 'w-full', 'py-3', 'rounded-lg', 'font-bold', 'text-lg');
button.onclick = () => handleChoice(choice);
choicesContainerEl.appendChild(button);
});
}
}
/**
* Initiates a Quick Time Event.
* @param {object} qte - The QTE data (type, duration, key).
*/
function startQTE(qte) {
qteActive = true;
qteUIEl.classList.remove('hidden');
choicesContainerEl.classList.add('hidden');
qteKeyEl.textContent = qte.key === " " ? "SPACEBAR" : qte.key.toUpperCase();
qteInstructionEl.textContent = "Keep them quiet!";
qteProgress = qteMaxProgress;
qteProgressEl.style.width = `${qteProgress}%`;
qteInterval = setInterval(() => {
qteProgress -= 1; // Drain the bar
qteProgressEl.style.width = `${qteProgress}%`;
if (qteProgress <= 0) {
endQTE(false);
}
}, qte.duration / qteMaxProgress);
// Listen for the correct key press
document.addEventListener('keydown', handleQTEKey);
}
/**
* Handles the key press during a Quick Time Event.
* @param {KeyboardEvent} event - The keydown event.
*/
function handleQTEKey(event) {
if (!qteActive) return;
const qteKey = gameEvents[currentChoice].qte.key;
if (event.key.toLowerCase() === qteKey.toLowerCase()) {
qteProgress += 2; // Increase bar progress with each press
if (qteProgress > qteMaxProgress) {
qteProgress = qteMaxProgress;
}
qteProgressEl.style.width = `${qteProgress}%`;
if (qteProgress >= qteMaxProgress) {
endQTE(true);
}
}
}
/**
* Ends the Quick Time Event and processes the result.
* @param {boolean} success - True if the user succeeded, false otherwise.
*/
function endQTE(success) {
if (!qteActive) return;
qteActive = false;
clearInterval(qteInterval);
clearTimeout(qteTimeout);
document.removeEventListener('keydown', handleQTEKey);
qteUIEl.classList.add('hidden');
if (success) {
// Correct choice logic for QTE
storyTextEl.textContent = "Your lightning-fast reaction saves the day, for now...";
handleChoice(gameEvents[currentChoice].correctChoice);
} else {
// Wrong choice logic for QTE
storyTextEl.textContent = "You were too slow. Your hesitation costs you dearly.";
handleWrongChoice();
}
}
/**
* Increases the bond level with a specified character.
* @param {string} characterName - The name of the character.
*/
function increaseBond(characterName) {
const protagonist = survivors.find(s => s.isProtagonist);
if (protagonist) {
const currentBond = protagonist.bonds[characterName];
const currentIndex = bondLevels.indexOf(currentBond);
if (currentIndex < bondLevels.length - 1) {
protagonist.bonds[characterName] = bondLevels[currentIndex + 1];
}
}
}
/**
* Decreases the bond level with a specified character.
* @param {string} characterName - The name of the character.
*/
function decreaseBond(characterName) {
const protagonist = survivors.find(s => s.isProtagonist);
if (protagonist) {
const currentBond = protagonist.bonds[characterName];
const currentIndex = bondLevels.indexOf(currentBond);
if (currentIndex > 0) {
protagonist.bonds[characterName] = bondLevels[currentIndex - 1];
}
}
}
/**
* Handles the user's choice and updates the game state.
* @param {string} choice - The user's chosen option.
*/
function handleChoice(choice) {
if (gameOver || qteActive) return;
const event = gameEvents[currentChoice];
if (choice !== event.correctChoice) {
handleWrongChoice();
} else {
// Correct choice logic: increase bond with a relevant character
switch (currentChoice) {
case 0: increaseBond("Alex"); break; // Correctly choosing to crawl into the air vent increases bond with Alex
case 1: increaseBond("Sarah"); break; // Waiting for monster to pass increases bond with Sarah
case 2: increaseBond("Maria"); break; // Searching for map increases bond with Maria
case 3: increaseBond("Alex"); break; // Sticking together increases bond with Alex
case 4: increaseBond("Jake"); break; // Using forklift increases bond with Jake
case 5: increaseBond("Alex"); break; // QTE success increases bond with Alex
case 6: increaseBond("Chloe"); break; // Climbing pipe network increases bond with Chloe
case 7: increaseBond("Leo"); break; // QTE success increases bond with Leo
case 8: increaseBond("Chloe"); break; // Pressing button increases bond with Chloe
case 9: increaseBond("Maria"); break; // Finding the exit increases bond with Maria
case 10: increaseBond("Jake"); break; // Forcing gate open increases bond with Jake
case 11: increaseBond("Sarah"); break; // QTE success increases bond with Sarah
case 12: increaseBond("Leo"); break; // Hiding in hollow log increases bond with Leo
case 13: increaseBond("Jake"); break; // Barricading the door increases bond with Jake
case 14: increaseBond("Alex"); break; // Using the gas can increases bond with Alex
case 15: increaseBond("Ben"); break; // Climbing out window increases bond with Ben
case 16: increaseBond("Chloe"); break; // Luring monster off cliff increases bond with Chloe
case 17: increaseBond("Alex"); break; // Finding town increases bond with Alex
case 18: increaseBond("Maria"); break; // Flagging down truck increases bond with Maria
case 19:
// Special logic for extended ending trigger
const amieAlive = getSurvivorStatus("Amie") === "alive";
const alexAlive = getSurvivorStatus("Alex") === "alive";
const jakeAlive = getSurvivorStatus("Jake") === "alive";
const mariaAlive = getSurvivorStatus("Maria") === "alive";
const allAlive = amieAlive && alexAlive && jakeAlive && mariaAlive;
const bondsAreBest = getBondLevel("Amie", "Alex") === "best" && getBondLevel("Amie", "Jake") === "best" && getBondLevel("Amie", "Maria") === "best";
if (allAlive && bondsAreBest) {
isSuperSecretEnding = true;
choicesMadeEl.textContent = `Choice: 20 / 30`;
storyTextEl.textContent = "You've done it! The monster has returned, but so have your allies. Prepare for the final battle!";
setTimeout(() => {
currentChoice++;
renderGame();
}, 4000);
} else {
// Proceed to regular ending
proceedToNextChoice();
}
return; // Important: Don't proceed to next choice yet
}
storyTextEl.textContent = "Your choice pays off, for now...";
proceedToNextChoice();
}
}
/**
* Executes the logic for a wrong choice.
*/
function handleWrongChoice() {
const event = gameEvents[currentChoice];
wrongChoices++;
const victim = survivors.find(s => s.name === event.victimName);
const livingSurvivors = survivors.filter(s => s.status === 'alive' && !s.isProtagonist);
let consequenceText = "";
let finalVictimName = event.victimName;
// Use the dynamic consequence function
if (typeof event.wrongConsequence === 'function') {
consequenceText = event.wrongConsequence(survivors);
} else {
consequenceText = event.wrongConsequence;
}
// If the intended victim is dead, select a random new victim
if (victim && victim.status === 'deceased' && livingSurvivors.length > 0) {
const newVictim = livingSurvivors[Math.floor(Math.random() * livingSurvivors.length)];
newVictim.status = 'deceased';
consequenceText = `Your choice leads to disaster. The monster finds a weak spot in your group and eliminates ${newVictim.name}. You just lost a friend.`;
finalVictimName = newVictim.name;
} else if (victim) {
victim.status = 'deceased';
}
// Injury logic is now more dynamic and applies to remaining alive members
const livingNonProtagonists = survivors.filter(s => s.status === 'alive' && !s.isProtagonist && s.name !== finalVictimName);
if (livingNonProtagonists.length > 0) {
const injuredVictim = livingNonProtagonists[Math.floor(Math.random() * livingNonProtagonists.length)];
injuredVictim.status = 'injured';
consequenceText += ` ${injuredVictim.name} is injured in the chaos.`;
}
storyTextEl.textContent = consequenceText;
// Decrease bond with the final victim
if (finalVictimName) decreaseBond(finalVictimName);
updateBackgroundColor();
proceedToNextChoice();
}
/**
* Moves the game to the next choice.
*/
function proceedToNextChoice() {
currentChoice++;
const remainingSurvivors = survivors.filter(s => s.status === 'alive' || s.status === 'injured').length;
if (currentChoice >= gameEvents.length || remainingSurvivors <= 1) {
endGame();
} else {
setTimeout(renderGame, 4000);
}
}
/**
* Updates the background color based on the number of wrong choices.
*/
function updateBackgroundColor() {
// Map the number of wrong choices (0-20) to a darkness value
const darkness = Math.min(wrongChoices, 20);
// Interpolate from a dark gray to a deep red
const startColor = { r: 11, g: 12, b: 16 };
const endColor = { r: 139, g: 0, b: 0 };
const r = Math.round(startColor.r + (endColor.r - startColor.r) * (darkness / 20));
const g = Math.round(startColor.g + (endColor.g - startColor.g) * (darkness / 20));
const b = Math.round(startColor.b + (endColor.b - startColor.b) * (darkness / 20));
body.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
}
/**
* Ends the game and displays the final message.
*/
function endGame() {
gameOver = true;
choicesContainerEl.innerHTML = '';
const livingSurvivors = survivors.filter(s => s.status === 'alive' || s.status === 'injured');
const protagonist = survivors.find(s => s.isProtagonist);
const remainingNames = livingSurvivors.map(s => s.name);
const benAlive = remainingNames.includes("Ben");
const amieAlive = remainingNames.includes("Amie");
const alexAlive = remainingNames.includes("Alex");
const jakeAlive = remainingNames.includes("Jake");
const mariaAlive = remainingNames.includes("Maria");
const chloeAlive = remainingNames.includes("Chloe");
const leoAlive = remainingNames.includes("Leo");
const sarahAlive = remainingNames.includes("Sarah");
let message = "";
let messageClass = "";
if (isSuperSecretEnding) {
if (amieAlive && alexAlive && jakeAlive && mariaAlive && getBondLevel("Amie", "Alex") === "best" && getBondLevel("Amie", "Jake") === "best" && getBondLevel("Amie", "Maria") === "best") {
message = "Super Secret Ending: The Golden Four. You have not only survived, but you have destroyed the monster once and for all. Your bond and unwavering trust have saved the world. You have found a new purpose, and a new life together.";
messageClass = 'text-purple-500';
} else if (amieAlive && alexAlive && jakeAlive) {
message = "Super Secret Ending: The Sacrifice. You have destroyed the monster, but Maria perished in the final fight. You and your friends have survived, but the victory is bittersweet. Her memory will live on with you.";
messageClass = 'text-purple-500';
} else if (amieAlive && alexAlive && mariaAlive) {
message = "Super Secret Ending: The Sacrifice. You have destroyed the monster, but Jake perished in the final fight. You and your friends have survived, but the victory is bittersweet. His memory will live on with you.";
messageClass = 'text-purple-500';
} else if (amieAlive && jakeAlive && mariaAlive) {
message = "Super Secret Ending: The Sacrifice. You have destroyed the monster, but Alex perished in the final fight. You and your friends have survived, but the victory is bittersweet. His memory will live on with you.";
messageClass = 'text-purple-500';
} else if (amieAlive && alexAlive) {
message = "Super Secret Ending: The Sacrifice. You have destroyed the monster, but Jake and Maria perished in the final fight. You and Alex have survived, but the victory is bittersweet. They will live on in your hearts.";
messageClass = 'text-purple-500';
} else if (amieAlive && jakeAlive) {
message = "Super Secret Ending: The Sacrifice. You have destroyed the monster, but Alex and Maria perished in the final fight. You and Jake have survived, but the victory is bittersweet. They will live on in your hearts.";
messageClass = 'text-purple-500';
} else if (amieAlive && mariaAlive) {
message = "Super Secret Ending: The Sacrifice. You have destroyed the monster, but Alex and Jake perished in the final fight. You and Maria have survived, but the victory is bittersweet. They will live on in your hearts.";
messageClass = 'text-purple-500';
} else if (alexAlive && jakeAlive && mariaAlive) {
message = "Super Secret Ending: The Leader's Sacrifice. Amie perished in the final battle to save you. Her sacrifice was not in vain, as you have destroyed the monster once and for all. You will honor her memory forever.";
messageClass = 'text-purple-500';
} else if (amieAlive) {
message = "Super Secret Ending: Amie's Heroic Sacrifice. You are the only one left. Your friends perished in the final battle, but you have destroyed the monster. You have saved the world, but at a great cost. Your name will be remembered forever.";
messageClass = 'text-purple-500';
} else if (alexAlive) {
message = "Super Secret Ending: Alex's Heroic Sacrifice. You are the only one left. Your friends perished in the final battle, but you have destroyed the monster. You have saved the world, but at a great cost. Your name will be remembered forever.";
messageClass = 'text-purple-500';
} else if (jakeAlive) {
message = "Super Secret Ending: Jake's Heroic Sacrifice. You are the only one left. Your friends perished in the final battle, but you have destroyed the monster. You have saved the world, but at a great cost. Your name will be remembered forever.";
messageClass = 'text-purple-500';
} else if (mariaAlive) {
message = "Super Secret Ending: Maria's Heroic Sacrifice. You are the only one left. Your friends perished in the final battle, but you have destroyed the monster. You have saved the world, but at a great cost. Your name will be remembered forever.";
messageClass = 'text-purple-500';
} else {
message = "Super Secret Ending: No One Survived. You failed. The monster has won. The world is doomed. Replay to find a better ending.";
messageClass = 'text-red-500';
}
} else {
// --- Secret Endings Logic ---
// Amie Lover Endings
if (amieAlive && alexAlive && getBondLevel("Amie", "Alex") === "best" && remainingNames.length === 2) {
message = "Secret Ending: Amie and Alex. You two are the only survivors, and your bond is unbreakable. You fall in love and find solace in each other, surviving not just the monster, but the loneliness of a new world.";
messageClass = 'text-blue-500';
} else if (amieAlive && jakeAlive && getBondLevel("Amie", "Jake") === "best" && remainingNames.length === 2) {
message = "Secret Ending: Amie and Jake. Your survival has forged a deep connection. You start a new life together, and your shared history becomes the foundation of a new love.";
messageClass = 'text-blue-500';
} else if (amieAlive && mariaAlive && getBondLevel("Amie", "Maria") === "best" && remainingNames.length === 2) {
message = "Secret Ending: Amie and Maria. Through fire and terror, you found each other. Your friendship blossoms into a deep, loving bond as you face the future together.";
messageClass = 'text-blue-500';
}
// Other Lover Endings
else if (jakeAlive && alexAlive && getBondLevel("Jake", "Alex") === "best" && remainingNames.length === 2) {
message = "Secret Ending: Jake and Alex. Your mutual respect and teamwork have led to a secret love that blossomed in the most dangerous of times. Your bond is a beacon of hope.";
messageClass = 'text-blue-500';
} else if (jakeAlive && mariaAlive && getBondLevel("Jake", "Maria") === "best" && remainingNames.length === 2) {
message = "Secret Ending: Jake and Maria. The fight for survival brought you closer than you ever thought possible. You are each other's home now, and your love will outlast the terror you faced.";
messageClass = 'text-blue-500';
}
// Ben the Psycho Endings (only if less than 5 survive with Ben)
else if (benAlive && remainingNames.length >= 2 && remainingNames.length <= 4) {
message = "Secret Ending: Ben the Psycho. You escaped with Ben, but you should have left him. In a fit of paranoid rage, he kills the remaining survivors before turning the weapon on himself. You are left alone with the bodies.";
messageClass = 'text-red-500';
} else if (benAlive && remainingNames.length === 1) {
message = "Secret Ending: Ben's Lonely End. Ben is the last survivor. The trauma of the event, combined with his paranoia, is too much. He takes his own life, alone in the wilderness, haunted by the ghosts of his friends.";
messageClass = 'text-red-500';
}
// Alone Endings
else if (amieAlive && remainingNames.length === 1) {
message = "Secret Ending: Amie Alone. You escaped, but you are the only one left. You sit on the road to town, sobbing, as you realize the cost of your survival. The trauma will be with you forever.";
messageClass = 'text-red-500';
} else if (jakeAlive && remainingNames.length === 1) {
message = "Secret Ending: Jake Alone. You are the last one. You escaped, but your heart is filled with rage. You swear vengeance on the monster, vowing to hunt it down and destroy it, no matter the cost.";
messageClass = 'text-red-500';
} else if (mariaAlive && remainingNames.length === 1) {
message = "Secret Ending: Maria Alone. You escaped, but your tears flow freely. You mourn your friends, and a heavy sadness settles over you. You are alive, but you are utterly alone.";
messageClass = 'text-red-500';
}
// Other Character Endings
else if (chloeAlive && remainingNames.length === 1) {
message = "Secret Ending: Chloe Alone. You are the last one. You escaped, but a sense of emptiness consumes you. You're free from the monster, but now you must live with the silence.";
messageClass = 'text-red-500';
} else if (leoAlive && remainingNames.length === 1) {
message = "Secret Ending: Leo Alone. You're the last survivor. You managed to escape, but the weight of the night's events is unbearable. You find a quiet place to sit, and reflect on the memories of those who didn't make it.";
messageClass = 'text-red-500';
} else if (alexAlive && remainingNames.length === 1) {
message = "Secret Ending: Alex Alone. You are the last survivor. You stand on the road to town, bruised and battered. You have survived, but the joy of survival is replaced by the overwhelming solitude. You have lost everyone.";
messageClass = 'text-red-500';
} else if (sarahAlive && remainingNames.length === 1) {
message = "Secret Ending: Sarah Alone. You are the last survivor. You find yourself alone on the road, your body trembling with fear and exhaustion. You made it, but the experience has left you shattered. You have lost everything.";
messageClass = 'text-red-500';
}
// Standard Endings
else if (livingSurvivors.length === 8) {
message = "An Unbelievable Victory! All of you made it out alive. You are forever bound by this experience. This is the best ending!";
messageClass = 'text-green-500';
} else if (livingSurvivors.length >= 5) {
message = `Best Ending: You survived with ${livingSurvivors.length} allies! You have found a new purpose in life and will live with the memory of the fallen.`;
messageClass = 'text-green-500';
} else if (livingSurvivors.length >= 2) {
message = `Neutral Ending: You survived with ${livingSurvivors.length} allies. Your survival is bittersweet, as the trauma of the night will haunt you forever.`;
messageClass = 'text-yellow-500';
} else {
message = `Worst Ending: Only ${livingSurvivors.length} survivors remain. The group is shattered. You escaped, but you are all alone.`;
messageClass = 'text-red-500';
}
}
gameMessageEl.textContent = message;
gameMessageEl.className = `hidden mt-6 text-xl md:text-2xl font-bold fade-in ${messageClass}`;
gameMessageEl.classList.remove('hidden');
restartButtonEl.classList.remove('hidden');
}
// --- Event Listeners ---
restartButtonEl.addEventListener('click', startGame);
// --- Start the game on page load ---
window.onload = startGame;
</script>
</body>
</html>