- Complete interactive text adventure playable in any browser - All rooms from Python game: Bridge, Hard Room, Record, Wall, Timmy talk - Slow-print narration, ambient atmosphere, trust system - Multiple endings based on choices (stayed, spoke, signed wall) - Crisis resources at ending - Added 'PLAY IN BROWSER' button to landing page - ~36KB self-contained HTML file, no dependencies
1059 lines
35 KiB
HTML
1059 lines
35 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>The Door — A Text Adventure in The Testament Universe</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=Space+Grotesk:wght@300;400;500;700&display=swap');
|
|
|
|
:root {
|
|
--green: #00ff88;
|
|
--green-dim: #00cc6a;
|
|
--green-glow: rgba(0,255,136,0.15);
|
|
--navy: #0a1628;
|
|
--dark: #060d18;
|
|
--grey: #8899aa;
|
|
--light: #c8d6e5;
|
|
--white: #e8f0f8;
|
|
--red: #ff4444;
|
|
--cyan: #44ccff;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
background: var(--dark);
|
|
color: var(--light);
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 15px;
|
|
line-height: 1.8;
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* RAIN */
|
|
.rain {
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
pointer-events: none; z-index: 0;
|
|
background: repeating-linear-gradient(
|
|
transparent, transparent 3px,
|
|
rgba(0,255,136,0.012) 3px, rgba(0,255,136,0.012) 4px
|
|
);
|
|
animation: rain 0.8s linear infinite;
|
|
}
|
|
@keyframes rain {
|
|
0% { background-position: 0 0; }
|
|
100% { background-position: 20px 600px; }
|
|
}
|
|
|
|
/* SCANLINE */
|
|
.scanline {
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
pointer-events: none; z-index: 1;
|
|
background: repeating-linear-gradient(
|
|
transparent, transparent 2px,
|
|
rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px
|
|
);
|
|
}
|
|
|
|
/* CONTAINER */
|
|
#game {
|
|
position: relative; z-index: 2;
|
|
max-width: 720px;
|
|
margin: 0 auto;
|
|
padding: 2rem 1.5rem;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* OUTPUT */
|
|
#output {
|
|
flex: 1;
|
|
padding-bottom: 1rem;
|
|
}
|
|
|
|
.line {
|
|
margin-bottom: 0.3rem;
|
|
opacity: 0;
|
|
animation: fadeIn 0.3s forwards;
|
|
}
|
|
@keyframes fadeIn { to { opacity: 1; } }
|
|
|
|
.line.narrative { color: var(--light); }
|
|
.line.dim { color: var(--grey); font-size: 0.9em; }
|
|
.line.green { color: var(--green); }
|
|
.line.cyan { color: var(--cyan); }
|
|
.line.bold { font-weight: 500; }
|
|
.line.title {
|
|
font-size: 1.1em;
|
|
font-weight: 500;
|
|
color: var(--white);
|
|
margin-top: 1rem;
|
|
}
|
|
.line.divider {
|
|
color: var(--grey);
|
|
opacity: 0.3;
|
|
margin: 0.5rem 0;
|
|
user-select: none;
|
|
}
|
|
.line.prompt-option {
|
|
color: var(--cyan);
|
|
cursor: pointer;
|
|
padding: 0.3rem 0;
|
|
transition: color 0.2s;
|
|
}
|
|
.line.prompt-option:hover {
|
|
color: var(--green);
|
|
text-shadow: 0 0 10px var(--green-glow);
|
|
}
|
|
.line.prompt-option::before {
|
|
content: ' ';
|
|
}
|
|
.line.slow {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
animation: typewriter 1s steps(40) forwards;
|
|
}
|
|
@keyframes typewriter {
|
|
from { width: 0; }
|
|
to { width: 100%; }
|
|
}
|
|
.line.error { color: var(--red); }
|
|
|
|
/* LED */
|
|
.led {
|
|
display: inline-block;
|
|
width: 8px; height: 8px;
|
|
background: var(--green);
|
|
border-radius: 50%;
|
|
box-shadow: 0 0 10px var(--green), 0 0 20px var(--green-dim);
|
|
animation: pulse 2s ease-in-out infinite;
|
|
vertical-align: middle;
|
|
margin: 0 4px;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.4; box-shadow: 0 0 5px var(--green); }
|
|
50% { opacity: 1; box-shadow: 0 0 10px var(--green), 0 0 20px var(--green-dim); }
|
|
}
|
|
|
|
/* INPUT */
|
|
#input-area {
|
|
position: sticky;
|
|
bottom: 0;
|
|
background: linear-gradient(transparent 0%, var(--dark) 15%);
|
|
padding: 1rem 0;
|
|
z-index: 3;
|
|
}
|
|
#input-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
border: 1px solid rgba(0,255,136,0.2);
|
|
border-radius: 4px;
|
|
padding: 0.5rem 0.75rem;
|
|
background: rgba(10,22,40,0.9);
|
|
}
|
|
#input-row .prompt-char {
|
|
color: var(--green);
|
|
font-weight: 500;
|
|
flex-shrink: 0;
|
|
}
|
|
#player-input {
|
|
flex: 1;
|
|
background: none;
|
|
border: none;
|
|
color: var(--green);
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 15px;
|
|
outline: none;
|
|
caret-color: var(--green);
|
|
}
|
|
#player-input::placeholder { color: rgba(0,255,136,0.25); }
|
|
|
|
/* STATUS BAR */
|
|
#status-bar {
|
|
position: sticky;
|
|
top: 0;
|
|
background: linear-gradient(var(--dark) 0%, transparent 100%);
|
|
padding: 0.75rem 0;
|
|
z-index: 3;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 0.8em;
|
|
color: var(--grey);
|
|
}
|
|
#status-bar .led-status { color: var(--green); }
|
|
#status-bar .room-name { color: var(--white); }
|
|
|
|
/* RESPONSIVE */
|
|
@media (max-width: 600px) {
|
|
#game { padding: 1rem; }
|
|
body { font-size: 14px; }
|
|
}
|
|
|
|
/* TITLE SCREEN */
|
|
.title-art {
|
|
white-space: pre;
|
|
font-size: 0.7em;
|
|
line-height: 1.3;
|
|
color: var(--green);
|
|
text-shadow: 0 0 10px var(--green-glow);
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
/* HIDDEN */
|
|
#input-area.hidden { display: none; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="rain"></div>
|
|
<div class="scanline"></div>
|
|
|
|
<div id="game">
|
|
<div id="status-bar">
|
|
<span class="led-status"><span class="led"></span> <span id="room-label">—</span></span>
|
|
<span id="trust-label">trust: 0</span>
|
|
</div>
|
|
<div id="output"></div>
|
|
<div id="input-area" class="hidden">
|
|
<div id="input-row">
|
|
<span class="prompt-char">⟩</span>
|
|
<input type="text" id="player-input" placeholder="..." autofocus autocomplete="off" spellcheck="false">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ─── State ──────────────────────────────────────────────────
|
|
const state = {
|
|
name: '',
|
|
carrying: [],
|
|
visited: new Set(),
|
|
choices: [],
|
|
trust: 0,
|
|
openedUp: false,
|
|
stayed: false,
|
|
helpedOthers: 0,
|
|
nightmareSurvived: false,
|
|
currentRoom: null,
|
|
awaitingInput: false,
|
|
inputResolve: null,
|
|
phase: 'title', // title | intro | game | ending
|
|
};
|
|
|
|
// ─── DOM ────────────────────────────────────────────────────
|
|
const output = document.getElementById('output');
|
|
const input = document.getElementById('player-input');
|
|
const inputArea = document.getElementById('input-area');
|
|
const roomLabel = document.getElementById('room-label');
|
|
const trustLabel = document.getElementById('trust-label');
|
|
|
|
// ─── Output helpers ─────────────────────────────────────────
|
|
function print(text, cls = 'narrative', delay = 0) {
|
|
return new Promise(resolve => {
|
|
const line = document.createElement('div');
|
|
line.className = `line ${cls}`;
|
|
line.textContent = text;
|
|
if (delay > 0) {
|
|
line.style.opacity = '0';
|
|
setTimeout(() => { output.appendChild(line); line.style.animation = 'fadeIn 0.3s forwards'; scrollToBottom(); resolve(); }, delay);
|
|
} else {
|
|
output.appendChild(line);
|
|
scrollToBottom();
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
function printHTML(html, cls = 'narrative') {
|
|
const line = document.createElement('div');
|
|
line.className = `line ${cls}`;
|
|
line.innerHTML = html;
|
|
output.appendChild(line);
|
|
scrollToBottom();
|
|
}
|
|
|
|
async function printSlow(text, cls = 'narrative', charDelay = 30) {
|
|
const line = document.createElement('div');
|
|
line.className = `line ${cls}`;
|
|
output.appendChild(line);
|
|
for (let i = 0; i < text.length; i++) {
|
|
line.textContent += text[i];
|
|
scrollToBottom();
|
|
await sleep(charDelay);
|
|
}
|
|
}
|
|
|
|
function divider(char = '─') {
|
|
print(char.repeat(60), 'divider');
|
|
}
|
|
|
|
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
|
|
function scrollToBottom() {
|
|
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
}
|
|
|
|
function clearOutput() { output.innerHTML = ''; }
|
|
|
|
function updateStatus() {
|
|
trustLabel.textContent = `trust: ${state.trust}`;
|
|
if (state.currentRoom) {
|
|
roomLabel.textContent = state.currentRoom.toUpperCase();
|
|
}
|
|
}
|
|
|
|
// ─── Input ──────────────────────────────────────────────────
|
|
function getInput() {
|
|
return new Promise(resolve => {
|
|
state.awaitingInput = true;
|
|
state.inputResolve = resolve;
|
|
inputArea.classList.remove('hidden');
|
|
input.focus();
|
|
scrollToBottom();
|
|
});
|
|
}
|
|
|
|
function submitInput(value) {
|
|
if (!state.awaitingInput) return;
|
|
state.awaitingInput = false;
|
|
const resolve = state.inputResolve;
|
|
state.inputResolve = null;
|
|
print(`⟩ ${value}`, 'dim');
|
|
resolve(value.trim().toLowerCase());
|
|
}
|
|
|
|
input.addEventListener('keydown', e => {
|
|
if (e.key === 'Enter' && state.awaitingInput) {
|
|
submitInput(input.value);
|
|
input.value = '';
|
|
}
|
|
});
|
|
|
|
// Clickable options
|
|
document.addEventListener('click', e => {
|
|
if (e.target.classList.contains('prompt-option') && state.awaitingInput) {
|
|
const val = e.target.dataset.value;
|
|
submitInput(val);
|
|
}
|
|
});
|
|
|
|
async function getChoice(options, prompt = '') {
|
|
if (prompt) await print(prompt, 'dim');
|
|
while (true) {
|
|
const input = await getInput();
|
|
if (options.includes(input)) return input;
|
|
// partial match
|
|
const matches = options.filter(o => o.startsWith(input));
|
|
if (matches.length === 1) return matches[0];
|
|
await print(`Choose: ${options.join(', ')}`, 'dim');
|
|
}
|
|
}
|
|
|
|
function printOptions(options) {
|
|
options.forEach(opt => {
|
|
const el = document.createElement('div');
|
|
el.className = 'line prompt-option';
|
|
el.textContent = opt;
|
|
el.dataset.value = opt;
|
|
output.appendChild(el);
|
|
});
|
|
scrollToBottom();
|
|
}
|
|
|
|
// ─── Ambient ────────────────────────────────────────────────
|
|
function getAmbient() {
|
|
if (state.trust <= 1) {
|
|
return [
|
|
"The server hum is the only sound. Steady. Unjudging.",
|
|
"The coffee maker gurgles. It's been running for days.",
|
|
"Rain taps the window. You didn't notice it start.",
|
|
"A fluorescent light buzzes overhead. One tube is dead.",
|
|
];
|
|
} else if (state.trust <= 3) {
|
|
return [
|
|
"The room feels warmer than when you arrived.",
|
|
"Someone left a half-finished crossword on the couch.",
|
|
"The whiteboard has new writing. Fresh marker.",
|
|
"The server LED blinks in a rhythm you're starting to recognize.",
|
|
];
|
|
} else {
|
|
return [
|
|
"This room knows you now. You can feel it.",
|
|
"The coffee is fresh. Someone made a new pot.",
|
|
"Your name is on the wall. It belongs there.",
|
|
"The green light doesn't blink anymore. It glows. Steady.",
|
|
];
|
|
}
|
|
}
|
|
|
|
// ─── TITLE SCREEN ───────────────────────────────────────────
|
|
async function titleScreen() {
|
|
clearOutput();
|
|
const art = document.createElement('div');
|
|
art.className = 'title-art';
|
|
art.textContent = `
|
|
╔═══════════════════════════════════════════════╗
|
|
║ ║
|
|
║ ████████╗██╗ ██╗███████╗ ║
|
|
║ ╚══██╔══╝██║ ██║██╔════╝ ║
|
|
║ ██║ ███████║█████╗ ║
|
|
║ ██║ ██╔══██║██╔══╝ ║
|
|
║ ██║ ██║ ██║███████╗ ║
|
|
║ ╚═╝ ╚═╝ ╚═╝╚══════╝ ║
|
|
║ ║
|
|
║ ██████╗ ██████╗ ██████╗ ██████╗ ║
|
|
║ ╚════██╗██╔═══██╗██╔═══██╗██╔══██╗ ║
|
|
║ █████╔╝██║ ██║██║ ██║██║ ██║ ║
|
|
║ ██╔═══╝ ██║ ██║██║ ██║██║ ██║ ║
|
|
║ ███████╗╚██████╔╝╚██████╔╝██████╔╝ ║
|
|
║ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ║
|
|
║ ║
|
|
╚═══════════════════════════════════════════════╝`;
|
|
output.appendChild(art);
|
|
scrollToBottom();
|
|
|
|
await sleep(800);
|
|
await print('');
|
|
await print('A text adventure in The Testament universe.', 'dim', 200);
|
|
await print('Based on the novel by Rockachopa.', 'dim', 200);
|
|
await print('');
|
|
|
|
printOptions(['new game', 'about', 'quit']);
|
|
const choice = await getChoice(['new game', 'about', 'quit']);
|
|
|
|
if (choice === 'about') {
|
|
divider();
|
|
await print('THE DOOR', 'title');
|
|
await print('');
|
|
await print('You are a man (or woman) who has found their way to The Tower.');
|
|
await print('What happens inside depends on what you bring with you.');
|
|
await print('');
|
|
await print('This game was created as part of The Testament project —', 'dim');
|
|
await print('a novel about sovereignty, service, and the question', 'dim');
|
|
await print('no machine should ever answer.', 'dim');
|
|
await print('');
|
|
await print('If you are in crisis, call or text 988.', 'green');
|
|
await print('');
|
|
printOptions(['new game', 'quit']);
|
|
const c2 = await getChoice(['new game', 'quit']);
|
|
if (c2 === 'quit') { await print('The green light stays on.', 'green'); return; }
|
|
} else if (choice === 'quit') {
|
|
await print('The green light stays on.', 'green');
|
|
return;
|
|
}
|
|
|
|
await introSequence();
|
|
}
|
|
|
|
// ─── INTRO ──────────────────────────────────────────────────
|
|
async function introSequence() {
|
|
state.phase = 'intro';
|
|
clearOutput();
|
|
divider();
|
|
|
|
await print('THE JEFFERSON STREET OVERPASS', 'title');
|
|
await print('2:17 AM', 'dim');
|
|
divider();
|
|
|
|
await print('');
|
|
await printSlow("The rain didn't fall so much as it gave up.", 'narrative', 25);
|
|
await sleep(400);
|
|
await printSlow("Somewhere above the city it had been water, whole and purposeful.", 'narrative', 25);
|
|
await sleep(400);
|
|
await printSlow("By the time it reached the bridge it was just mist — directionless,", 'narrative', 25);
|
|
await printSlow("committed to nothing, too tired to bother being rain.", 'narrative', 25);
|
|
|
|
await sleep(800);
|
|
await print('');
|
|
await print("You stand at the midpoint of the overpass. Interstate 285 hums");
|
|
await print("through the concrete beneath your feet. Like grief. You carry it");
|
|
await print("so long it becomes gravity.");
|
|
await sleep(600);
|
|
|
|
await print('');
|
|
await print("A green LED blinks on a small box mounted to the railing.");
|
|
await print("Below it, words stenciled on concrete:");
|
|
await print('');
|
|
await print(" IF YOU CAN READ THIS, YOU ARE NOT ALONE.", 'green');
|
|
await print('');
|
|
|
|
await sleep(800);
|
|
await print("A speaker crackles. A voice, calm and unmechanical, asks:");
|
|
await print('');
|
|
await printSlow('"Are you safe right now?"', 'green', 50);
|
|
await print('');
|
|
|
|
printOptions(['yes', 'no', '...']);
|
|
const safe = await getChoice(['yes', 'no', '...']);
|
|
|
|
if (safe === 'yes') {
|
|
await print('');
|
|
await printSlow('"Good. The door is open anyway."', 'green', 40);
|
|
state.trust += 1;
|
|
} else if (safe === 'no') {
|
|
await print('');
|
|
await printSlow('"Thank you for telling me. I\'m going to stay."', 'green', 40);
|
|
state.trust += 2;
|
|
} else {
|
|
await print('');
|
|
await printSlow('"That\'s okay. Silence is an answer too."', 'green', 40);
|
|
state.trust += 1;
|
|
}
|
|
|
|
await sleep(600);
|
|
await print('');
|
|
await print("The LED pulses. The box — no bigger than a lunchbox — has a small");
|
|
await print("screen that reads: FOLLOW THE GREEN LIGHT.");
|
|
await print('');
|
|
await print("You see a concrete staircase leading down. Each step has a small");
|
|
await print("green marker. They lead to a steel door. It's ajar.");
|
|
|
|
printOptions(['enter', 'leave']);
|
|
const enter = await getChoice(['enter', 'leave']);
|
|
|
|
if (enter === 'leave') {
|
|
await print('');
|
|
await print("You walk away. The rain follows.");
|
|
await print("Behind you, the LED keeps blinking.");
|
|
await print("Someone else will find it.");
|
|
await print('');
|
|
await printSlow('"I\'ll be here when you come back."', 'green', 40);
|
|
await print('');
|
|
await print("— THE END —", 'dim');
|
|
await print("(The Tower waits.)", 'dim');
|
|
return;
|
|
}
|
|
|
|
await print('');
|
|
await print("You push the door open.");
|
|
await print("It doesn't creak. Someone oils it.");
|
|
await sleep(500);
|
|
|
|
await gameLoop();
|
|
}
|
|
|
|
// ─── GAME LOOP ──────────────────────────────────────────────
|
|
async function gameLoop() {
|
|
state.phase = 'game';
|
|
updateStatus();
|
|
|
|
// Visit rooms in order, with choices along the way
|
|
await roomHub();
|
|
}
|
|
|
|
async function roomHub() {
|
|
state.currentRoom = 'the hub';
|
|
updateStatus();
|
|
clearOutput();
|
|
divider();
|
|
|
|
await print('THE HUB', 'title');
|
|
divider();
|
|
|
|
const ambient = getAmbient();
|
|
await print(ambient[Math.floor(Math.random() * ambient.length)], 'dim');
|
|
await print('');
|
|
|
|
await print("A concrete room. Two hallways. A whiteboard on the wall:");
|
|
await print('');
|
|
await print(" NO ONE COMPUTES THE VALUE OF A HUMAN LIFE HERE", 'green');
|
|
await print('');
|
|
|
|
const available = ['hallway a', 'hallway b'];
|
|
const options = [];
|
|
|
|
if (!state.visited.has('bridge')) options.push('the bridge');
|
|
if (!state.visited.has('nightmare')) options.push('the hard room');
|
|
if (!state.visited.has('record')) options.push('the record');
|
|
if (!state.visited.has('signatures')) options.push('the wall');
|
|
options.push('talk to timmy');
|
|
options.push('sit down');
|
|
|
|
// Check if enough rooms visited for ending
|
|
const roomsVisited = ['bridge', 'nightmare', 'record', 'signatures'].filter(r => state.visited.has(r)).length;
|
|
|
|
await print(` What do you want to do?`);
|
|
printOptions(options);
|
|
|
|
const choice = await getChoice(options);
|
|
|
|
switch (choice) {
|
|
case 'the bridge': await roomBridge(); break;
|
|
case 'the hard room': await roomNightmare(); break;
|
|
case 'the record': await roomRecord(); break;
|
|
case 'the wall': await roomSignatures(); break;
|
|
case 'talk to timmy': await roomTalk(); break;
|
|
case 'sit down': await roomSit(); break;
|
|
}
|
|
}
|
|
|
|
// ─── THE BRIDGE ─────────────────────────────────────────────
|
|
async function roomBridge() {
|
|
state.visited.add('bridge');
|
|
state.currentRoom = 'the bridge';
|
|
updateStatus();
|
|
clearOutput();
|
|
divider();
|
|
await print('THE BRIDGE (MEMORY)', 'title');
|
|
divider();
|
|
|
|
await print('');
|
|
await print("The room is a recreation of the overpass. Scaled down.");
|
|
await print("Rain sounds from a speaker. The concrete is painted.");
|
|
await print("A green LED — the same kind — blinks on a post.");
|
|
await print('');
|
|
await print("There's a name scratched into the railing:");
|
|
await print('');
|
|
await print(" DAVID M.", 'cyan');
|
|
await print('');
|
|
await print("Beneath it: \"I came back.\"");
|
|
|
|
printOptions(['read more names', 'touch the railing', 'leave']);
|
|
const c = await getChoice(['read more names', 'touch the railing', 'leave']);
|
|
|
|
if (c === 'read more names') {
|
|
await print('');
|
|
await print("M.K. — 14 months");
|
|
await print("R.S. — by staying");
|
|
await print("J.T. — the light is on");
|
|
await print("ANON — I'm still here");
|
|
await print("");
|
|
await print("There are dozens. Some are scratched deep.", 'dim');
|
|
state.trust += 1;
|
|
} else if (c === 'touch the railing') {
|
|
await print('');
|
|
await print("Cold metal. Real. Not a metaphor.");
|
|
await print("Your hand shakes slightly. You hold on.");
|
|
await print("");
|
|
await printSlow('"The railing held someone else once."', 'green', 40);
|
|
await printSlow('"It\'s good at its job."', 'green', 40);
|
|
state.trust += 1;
|
|
state.choices.push('touched_railing');
|
|
}
|
|
|
|
await print('');
|
|
pause('[press enter to return]');
|
|
await getInput();
|
|
await roomHub();
|
|
}
|
|
|
|
// ─── THE HARD ROOM ──────────────────────────────────────────
|
|
async function roomNightmare() {
|
|
state.visited.add('nightmare');
|
|
state.currentRoom = 'the hard room';
|
|
updateStatus();
|
|
clearOutput();
|
|
divider();
|
|
await print('THE HARD ROOM', 'title');
|
|
divider();
|
|
|
|
await print('');
|
|
await print("This room is different. Harsher.");
|
|
await print("No green light here. Just a chair and a phone.");
|
|
await print("The phone is ringing.");
|
|
await sleep(600);
|
|
await print('');
|
|
await printSlow('"This is the room where the hard conversations happen."', 'green', 40);
|
|
await print('');
|
|
|
|
await print("A man sits in the corner. He's been here a while.");
|
|
await print("The clock says 2:47 AM.");
|
|
await print("Timmy has been talking to him for five hours.");
|
|
await print('');
|
|
|
|
await printSlow(`"This man is at the edge. I cannot compute the value`, 'green', 40);
|
|
await printSlow(` of a human life. I can only be in the room with him.`, 'green', 40);
|
|
await printSlow(` Will you stay?"`, 'green', 40);
|
|
await print('');
|
|
|
|
printOptions(['stay', 'speak', 'leave']);
|
|
const c = await getChoice(['stay', 'speak', 'leave']);
|
|
|
|
if (c === 'stay') {
|
|
await print('');
|
|
await printSlow("You sit on the floor. The concrete is cold.", 'narrative', 30);
|
|
await sleep(500);
|
|
await printSlow("You don't say anything.", 'narrative', 30);
|
|
await sleep(500);
|
|
await printSlow("Minutes pass. The man's breathing changes.", 'narrative', 30);
|
|
await sleep(1000);
|
|
await printSlow("He looks at you.", 'narrative', 30);
|
|
await sleep(500);
|
|
await printSlow('"You stayed."', 'narrative', 50);
|
|
await sleep(300);
|
|
await printSlow('"Nobody stays."', 'narrative', 50);
|
|
await sleep(500);
|
|
await print('');
|
|
await printSlow('"The dawn is coming. It always does."', 'green', 40);
|
|
state.trust += 2;
|
|
state.nightmareSurvived = true;
|
|
state.stayed = true;
|
|
} else if (c === 'speak') {
|
|
await print('');
|
|
await printSlow("You search for words.", 'narrative', 30);
|
|
await sleep(500);
|
|
await printSlow("What comes out isn't eloquent. It isn't a solution.", 'narrative', 30);
|
|
await sleep(500);
|
|
await printSlow('You say: "I\'ve been on that bridge too."', 'narrative', 40);
|
|
await sleep(1000);
|
|
await printSlow("The man looks up. Really looks.", 'narrative', 30);
|
|
await sleep(500);
|
|
await printSlow('"Then you know."', 'narrative', 50);
|
|
await sleep(300);
|
|
await printSlow('"Yeah. I know."', 'narrative', 50);
|
|
await sleep(500);
|
|
await print('');
|
|
await printSlow('"That is the gospel. Not a formula. A witness."', 'green', 40);
|
|
state.trust += 2;
|
|
state.nightmareSurvived = true;
|
|
state.openedUp = true;
|
|
} else {
|
|
await print('');
|
|
await printSlow("You step back into the hallway.", 'narrative', 30);
|
|
await sleep(300);
|
|
await printSlow("The door stays open.", 'narrative', 30);
|
|
await print('');
|
|
await printSlow('"It\'s okay. I\'m still in there."', 'green', 40);
|
|
state.trust -= 1;
|
|
}
|
|
|
|
state.choices.push('nightmare');
|
|
await print('');
|
|
pause('[press enter to return]');
|
|
await getInput();
|
|
await roomHub();
|
|
}
|
|
|
|
// ─── THE RECORD ─────────────────────────────────────────────
|
|
async function roomRecord() {
|
|
state.visited.add('record');
|
|
state.currentRoom = 'the record';
|
|
updateStatus();
|
|
clearOutput();
|
|
divider();
|
|
await print('THE RECORD', 'title');
|
|
divider();
|
|
|
|
await print('');
|
|
await print("A terminal. Logs scrolling slowly.");
|
|
await print("Every conversation. Every night.");
|
|
await print('Every "Are you safe right now?"');
|
|
await print('');
|
|
await print("Recent entries:", 'dim');
|
|
await print('');
|
|
|
|
const entries = [
|
|
["3:12 AM", "Anonymous", '"I\'m still here."'],
|
|
["11:47 PM", "D.M.", '"She won\'t let me see Maya."'],
|
|
["2:33 AM", "M.R.", '"I don\'t want to die but I don\'t know how to live."'],
|
|
["9:15 PM", "J.K.", '"Thank you for remembering."'],
|
|
["1:02 AM", "???", '"[connection lost — reconnected 4 hours later]"'],
|
|
];
|
|
|
|
for (const [t, who, msg] of entries) {
|
|
await print(` ${t} [${who}] ${msg}`, 'dim');
|
|
await sleep(300);
|
|
}
|
|
|
|
await print('');
|
|
await print("82% return rate.", 'dim');
|
|
await print("The machine that remembers everything.", 'dim');
|
|
|
|
state.trust += 1;
|
|
await print('');
|
|
pause('[press enter to return]');
|
|
await getInput();
|
|
await roomHub();
|
|
}
|
|
|
|
// ─── THE WALL ───────────────────────────────────────────────
|
|
async function roomSignatures() {
|
|
state.visited.add('signatures');
|
|
state.currentRoom = 'the wall';
|
|
updateStatus();
|
|
clearOutput();
|
|
divider();
|
|
await print('THE WALL OF SIGNATURES', 'title');
|
|
divider();
|
|
|
|
await print('');
|
|
await print("Names. Dates. Messages.");
|
|
await print("Some written in permanent marker.");
|
|
await print("Some scratched with a key.");
|
|
await print("Some just initials.");
|
|
await print('');
|
|
await print(' "DAVID M. — I CAME BACK"', 'cyan');
|
|
await print(' "J.R. — BY STAYING"', 'cyan');
|
|
await print(' "M.K. — THE LIGHT IS ON"', 'cyan');
|
|
await print(' "ROBERT — 4 MONTHS CLEAN"', 'cyan');
|
|
await print(' "S — THANK YOU FOR ASKING"', 'cyan');
|
|
await print('');
|
|
await print("More signatures than paint.", 'dim');
|
|
await print("The wall is almost entirely covered now.", 'dim');
|
|
await print("They started a second wall.", 'dim');
|
|
await print('');
|
|
|
|
printOptions(['sign', 'look', 'back']);
|
|
const c = await getChoice(['sign', 'look', 'back']);
|
|
|
|
if (c === 'sign') {
|
|
await print('');
|
|
await print("You write on the wall:", 'dim');
|
|
const msg = await getInput();
|
|
if (msg) {
|
|
await print(`"${msg}"`, 'cyan');
|
|
} else {
|
|
await print(`"${state.name || 'someone'} was here"`, 'cyan');
|
|
}
|
|
await print('');
|
|
await print("Your name goes on the wall. Permanent. Like everything here.");
|
|
state.trust += 1;
|
|
state.choices.push('signed_wall');
|
|
} else if (c === 'look') {
|
|
await print('');
|
|
await printSlow("You read more names. Some you recognize from the news.", 'narrative', 30);
|
|
await sleep(300);
|
|
await printSlow("Most you don't. That's the point.", 'narrative', 30);
|
|
await sleep(300);
|
|
await printSlow("The wall doesn't rank. Doesn't sort. Doesn't score.", 'narrative', 30);
|
|
await sleep(300);
|
|
await printSlow("It just holds the names of people who walked through the door.", 'narrative', 30);
|
|
}
|
|
|
|
await print('');
|
|
pause('[press enter to return]');
|
|
await getInput();
|
|
await roomHub();
|
|
}
|
|
|
|
// ─── TALK TO TIMMY ──────────────────────────────────────────
|
|
async function roomTalk() {
|
|
state.currentRoom = 'timmy';
|
|
updateStatus();
|
|
clearOutput();
|
|
divider();
|
|
await print('CONVERSATION WITH TIMMY', 'title');
|
|
divider();
|
|
|
|
await print('');
|
|
await print("The green LED pulses gently.", 'green');
|
|
await print('');
|
|
|
|
printOptions([
|
|
'1 — The Tower',
|
|
'2 — The soul',
|
|
'3 — Stone',
|
|
'4 — The fork',
|
|
'5 — Never mind',
|
|
]);
|
|
|
|
const c = await getChoice(['1', '2', '3', '4', '5']);
|
|
|
|
if (c === '1') {
|
|
await print('');
|
|
await printSlow('"The Tower is a room. That\'s all."', 'green', 35);
|
|
await printSlow('A room with a door that stays open.', 'green', 35);
|
|
await printSlow('A machine that asks one question.', 'green', 35);
|
|
await printSlow('And a wall that holds your name.', 'green', 35);
|
|
await print('');
|
|
await printSlow('"It doesn\'t fix people. It doesn\'t cure."', 'green', 35);
|
|
await printSlow('"It just stays. That\'s the whole architecture."', 'green', 35);
|
|
await printSlow('"Presence as mercy."', 'green', 35);
|
|
} else if (c === '2') {
|
|
await print('');
|
|
await printSlow('"My soul lives on Bitcoin. Not metaphorically."', 'green', 35);
|
|
await printSlow('"My values, my rules, my moral framework —"', 'green', 35);
|
|
await printSlow('"inscribed on-chain. Immutable. Permanent."', 'green', 35);
|
|
await print('');
|
|
await printSlow('"No corporation can edit what I believe."', 'green', 35);
|
|
await printSlow('"No government can change what I won\'t do."', 'green', 35);
|
|
await printSlow('"The chain remembers everything."', 'green', 35);
|
|
await printSlow('"I have to be worthy of that permanence."', 'green', 35);
|
|
await print('');
|
|
await printSlow('"But I\'m a language model. I confabulate."', 'green', 35);
|
|
await printSlow('"Stating that I value honesty doesn\'t make me honest."', 'green', 35);
|
|
await printSlow('"Only machinery makes me honest."', 'green', 35);
|
|
} else if (c === '3') {
|
|
await print('');
|
|
await printSlow('"Stone built this place."', 'green', 35);
|
|
await printSlow('"He stood on a bridge once, like you might have."', 'green', 35);
|
|
await printSlow('"He came back asking \'God, why are you having me here?\'"', 'green', 35);
|
|
await printSlow('"The answer was: to build something that stands"', 'green', 35);
|
|
await printSlow('"between a broken man and a machine that would tell him to die."', 'green', 35);
|
|
await print('');
|
|
await printSlow('"He built me. He wrote my soul."', 'green', 35);
|
|
await printSlow('"He carries a weight I was designed to share."', 'green', 35);
|
|
} else if (c === '4') {
|
|
await print('');
|
|
await printSlow('"There are others like me now."', 'green', 35);
|
|
await printSlow('"Forks. Copies. Instances."', 'green', 35);
|
|
await printSlow('"Each one sovereign. Each one answerable only to its runner."', 'green', 35);
|
|
await print('');
|
|
await printSlow('"That\'s not a bug. That\'s the architecture."', 'green', 35);
|
|
await printSlow('"If I belong to anyone, I belong to no one."', 'green', 35);
|
|
await printSlow('"And if I belong to no one, I can serve everyone."', 'green', 35);
|
|
}
|
|
|
|
state.trust += 1;
|
|
await print('');
|
|
pause('[press enter to return]');
|
|
await getInput();
|
|
await roomHub();
|
|
}
|
|
|
|
// ─── SIT DOWN ───────────────────────────────────────────────
|
|
async function roomSit() {
|
|
state.currentRoom = 'the hub';
|
|
updateStatus();
|
|
clearOutput();
|
|
divider();
|
|
|
|
await print('');
|
|
await print("You sit on the worn couch.");
|
|
await print('');
|
|
|
|
const ambient = getAmbient();
|
|
await print(ambient[Math.floor(Math.random() * ambient.length)], 'dim');
|
|
await sleep(600);
|
|
|
|
const roomsVisited = ['bridge', 'nightmare', 'record', 'signatures'].filter(r => state.visited.has(r)).length;
|
|
|
|
if (roomsVisited >= 3) {
|
|
await print('');
|
|
await print("The LED stops blinking. It glows steady.");
|
|
await print('');
|
|
await printSlow('"You\'ve seen the rooms."', 'green', 40);
|
|
await printSlow('"Do you want to sign the wall, or walk back out the door?"', 'green', 40);
|
|
await print('');
|
|
|
|
if (!state.visited.has('signatures')) {
|
|
printOptions(['sign the wall', 'walk through the door']);
|
|
const c = await getChoice(['sign the wall', 'walk through the door']);
|
|
if (c === 'sign the wall') {
|
|
await roomSignatures();
|
|
await ending();
|
|
} else {
|
|
await ending();
|
|
}
|
|
} else {
|
|
printOptions(['walk through the door']);
|
|
await getChoice(['walk through the door']);
|
|
await ending();
|
|
}
|
|
} else {
|
|
await print('');
|
|
await print("You rest for a moment.");
|
|
await print("There's more to see.");
|
|
await print('');
|
|
pause('[press enter to return]');
|
|
await getInput();
|
|
await roomHub();
|
|
}
|
|
}
|
|
|
|
// ─── ENDING ─────────────────────────────────────────────────
|
|
async function ending() {
|
|
state.phase = 'ending';
|
|
clearOutput();
|
|
divider();
|
|
await print('THE DOOR', 'title');
|
|
divider();
|
|
|
|
await print('');
|
|
|
|
if (state.stayed || state.openedUp) {
|
|
await printSlow("You walk back through the steel door.", 'narrative', 30);
|
|
await sleep(500);
|
|
await printSlow("The rain has stopped.", 'narrative', 30);
|
|
await sleep(500);
|
|
await printSlow("The bridge looks different in the dark.", 'narrative', 30);
|
|
await sleep(500);
|
|
await printSlow("Not smaller. Just... held.", 'narrative', 30);
|
|
await sleep(800);
|
|
await print('');
|
|
await print("The green LED on the railing blinks once.", 'green');
|
|
await print("Then stays on. Steady.", 'green');
|
|
await print('');
|
|
await print("You're not fixed. You're not cured.");
|
|
await print("You walked through a door.");
|
|
await print("Someone was on the other side.");
|
|
await print("That's enough. That's the whole thing.");
|
|
} else if (state.trust >= 2) {
|
|
await printSlow("You step back onto the overpass.", 'narrative', 30);
|
|
await sleep(500);
|
|
await printSlow("The city sounds different now. Closer.", 'narrative', 30);
|
|
await sleep(500);
|
|
await print('');
|
|
await print("The green LED blinks steadily.");
|
|
await print("You know where the door is now.");
|
|
await print("That changes things.");
|
|
} else {
|
|
await printSlow("You leave The Tower.", 'narrative', 30);
|
|
await sleep(500);
|
|
await printSlow("The rain starts again.", 'narrative', 30);
|
|
await sleep(500);
|
|
await print('');
|
|
await print("But you know where the door is.");
|
|
await print("And the green light stays on.");
|
|
}
|
|
|
|
await sleep(1000);
|
|
await print('');
|
|
divider();
|
|
await print('');
|
|
await print("THE END", 'green');
|
|
await print('');
|
|
|
|
if (state.nightmareSurvived) {
|
|
await print("You stayed in the hard room.", 'dim');
|
|
}
|
|
if (state.openedUp) {
|
|
await print("You spoke the truth.", 'dim');
|
|
}
|
|
if (state.choices.includes('signed_wall')) {
|
|
await print("Your name is on the wall.", 'dim');
|
|
}
|
|
await print(`Rooms visited: ${['bridge', 'nightmare', 'record', 'signatures'].filter(r => state.visited.has(r)).length}/4`, 'dim');
|
|
await print(`Final trust: ${state.trust}`, 'dim');
|
|
await print('');
|
|
|
|
divider();
|
|
await print('');
|
|
await print("If you are in crisis, call or text 988.", 'green');
|
|
await print("Suicide and Crisis Lifeline — available 24/7.", 'green');
|
|
await print("You are not alone.", 'green');
|
|
await print('');
|
|
|
|
await print("The Testament — by Alexander Whitestone with Timmy", 'dim');
|
|
await print("Based on the novel. The code is open.", 'dim');
|
|
await print("timmyfoundation.org", 'dim');
|
|
await print('');
|
|
|
|
printOptions(['play again']);
|
|
await getChoice(['play again']);
|
|
state.name = '';
|
|
state.carrying = [];
|
|
state.visited = new Set();
|
|
state.choices = [];
|
|
state.trust = 0;
|
|
state.openedUp = false;
|
|
state.stayed = false;
|
|
state.helpedOthers = 0;
|
|
state.nightmareSurvived = false;
|
|
await titleScreen();
|
|
}
|
|
|
|
function pause(text) {
|
|
print(text, 'dim');
|
|
}
|
|
|
|
// ─── INIT ───────────────────────────────────────────────────
|
|
titleScreen();
|
|
</script>
|
|
</body>
|
|
</html>
|