refactor for shared states and multi chapters

This commit is contained in:
Dan 2026-01-22 11:03:05 +00:00
parent 691b809b41
commit b9bea16f78
13 changed files with 2308 additions and 1786 deletions

View file

@ -5,12 +5,17 @@ class GameEngine {
this.terminal = null; this.terminal = null;
this.adapter = null; this.adapter = null;
this.state = null; this.state = null;
this.sharedState = null; // For series games
this.input = null; this.input = null;
this.sound = null; this.sound = null;
this.scenes = null; this.scenes = null;
this.isRunning = false; this.isRunning = false;
this.originalExecuteCommand = null; this.originalExecuteCommand = null;
// Series support
this.seriesId = gameDefinition.seriesId || null;
this.chapterNumber = gameDefinition.chapterNumber || null;
} }
// Register this game as a terminal command // Register this game as a terminal command
@ -35,6 +40,10 @@ class GameEngine {
self._reset(); self._reset();
return; return;
} }
if (args[0] === "reset-series" && self.seriesId) {
self._resetSeries();
return;
}
if (args[0] === "continue" || args[0] === "resume") { if (args[0] === "continue" || args[0] === "resume") {
await self.start(true); await self.start(true);
return; return;
@ -51,6 +60,23 @@ class GameEngine {
return; return;
} }
// Initialize shared state for series games (do this early for chapter check)
if (this.seriesId && window.SharedStateManager) {
this.sharedState = new SharedStateManager(this.seriesId);
this.sharedState.init(this.definition.sharedStateDefaults || {});
// Check if this chapter can be played (sequential unlock)
if (this.chapterNumber && this.chapterNumber > 1) {
if (!this.sharedState.canPlayChapter(this.chapterNumber)) {
const prevChapter = this.chapterNumber - 1;
this.terminal.printError(
`You must complete Chapter ${prevChapter} before playing Chapter ${this.chapterNumber}.`,
);
return;
}
}
}
// Update the global HTML to add the game in progress details // Update the global HTML to add the game in progress details
document.body.classList.add("game-in-progress"); document.body.classList.add("game-in-progress");
console.log(document.body.classList); console.log(document.body.classList);
@ -59,7 +85,7 @@ class GameEngine {
// Initialize components // Initialize components
this.adapter = new TerminalAdapter(this.terminal); this.adapter = new TerminalAdapter(this.terminal);
this.state = new StateManager(this.definition.id); this.state = new StateManager(this.definition.id, this.sharedState);
this.input = new InputManager(this.adapter); this.input = new InputManager(this.adapter);
// Initialize sound manager if SoundManager is available // Initialize sound manager if SoundManager is available
@ -77,8 +103,11 @@ class GameEngine {
// Initialize state // Initialize state
this.state.init(this.definition.initialState || {}); this.state.init(this.definition.initialState || {});
// Load and merge external scenes if defined
const mergedScenes = this._getMergedScenes();
// Register scenes // Register scenes
this.scenes.registerScenes(this.definition.scenes); this.scenes.registerScenes(mergedScenes);
// Hook into terminal input // Hook into terminal input
this._hookInput(); this._hookInput();
@ -236,6 +265,66 @@ class GameEngine {
); );
} }
// Reset entire series progress (for series games)
_resetSeries() {
// Reset chapter state
this._reset();
// Reset shared state
if (this.seriesId) {
const sharedState = new SharedStateManager(this.seriesId);
sharedState.reset();
this.terminal.printSuccess(
`All ${this.definition.name} series progress has been reset.`,
);
}
}
// Create context for scene factories
_createSceneContext() {
return {
chapterNumber: this.chapterNumber || 1,
seriesId: this.seriesId,
sharedState: this.sharedState,
art: this.definition.art || {},
additionalOptions: this.definition.additionalHubOptions || [],
};
}
// Load external scenes and merge with inline scenes
_getMergedScenes() {
const inlineScenes = this.definition.scenes || {};
// If no external scenes defined, just return inline scenes
if (
!this.definition.externalScenes ||
this.definition.externalScenes.length === 0
) {
return inlineScenes;
}
// Load external scenes from registered factories
const externalScenes = {};
const context = this._createSceneContext();
for (const sceneRef of this.definition.externalScenes) {
const factory = window.SceneFactories?.[sceneRef];
if (factory) {
try {
const scenes = factory(context);
Object.assign(externalScenes, scenes);
} catch (e) {
console.error(`Failed to load external scenes from ${sceneRef}:`, e);
}
} else {
console.warn(`Scene factory not found: ${sceneRef}`);
}
}
// Inline scenes override external scenes (allows chapter-specific overrides)
return { ...externalScenes, ...inlineScenes };
}
// Debug command to skip to a specific scene // Debug command to skip to a specific scene
async _debugGoToScene(sceneName) { async _debugGoToScene(sceneName) {
const scene = this.scenes.getScene(sceneName); const scene = this.scenes.getScene(sceneName);

View file

@ -322,6 +322,19 @@ class SceneManager {
this.state.set(action.set, action.value); this.state.set(action.set, action.value);
} }
// Set value in shared state (for series games)
if (action.setShared !== undefined) {
this.state.setShared(action.setShared, action.value);
}
// Mark a chapter as complete in shared state
if (action.markChapterComplete !== undefined) {
const sharedState = this.state.getSharedState();
if (sharedState) {
sharedState.markChapterComplete(action.markChapterComplete);
}
}
if (action.increment !== undefined) { if (action.increment !== undefined) {
this.state.increment(action.increment, action.amount || 1); this.state.increment(action.increment, action.amount || 1);
} }

View file

@ -0,0 +1,66 @@
// Scene Registry - Global registry for shared scene factories
// Scene factories are functions that take a context and return scene definitions
// Initialize global scene factories object
window.SceneFactories = window.SceneFactories || {};
// Helper class for working with scene factories
class SceneRegistry {
// Register a scene factory
static register(id, factory) {
if (typeof factory !== "function") {
console.error(`Scene factory must be a function: ${id}`);
return false;
}
window.SceneFactories[id] = factory;
return true;
}
// Get a scene factory
static get(id) {
return window.SceneFactories[id] || null;
}
// Check if a factory exists
static has(id) {
return id in window.SceneFactories;
}
// List all registered factories
static list() {
return Object.keys(window.SceneFactories);
}
// Unregister a factory
static unregister(id) {
delete window.SceneFactories[id];
}
// Create scenes from a factory with context
static createScenes(id, context) {
const factory = this.get(id);
if (!factory) {
console.warn(`Scene factory not found: ${id}`);
return {};
}
try {
return factory(context);
} catch (e) {
console.error(`Error creating scenes from factory ${id}:`, e);
return {};
}
}
// Merge multiple factories into one scene object
static mergeFactories(factoryIds, context) {
const merged = {};
for (const id of factoryIds) {
const scenes = this.createScenes(id, context);
Object.assign(merged, scenes);
}
return merged;
}
}
// Make available globally
window.SceneRegistry = SceneRegistry;

View file

@ -0,0 +1,190 @@
// Shared State Manager - Manages game state across multiple chapters in a series
class SharedStateManager {
constructor(seriesId) {
this.seriesId = seriesId;
this.state = {};
this.storageKey = `series_${seriesId}_state`;
}
// Initialize with default state (only sets values not already in storage)
init(defaultState = {}) {
this.state = this._deepClone(defaultState);
this._loadFromStorage();
}
// Get value using dot notation path (e.g., "inventory.sword")
get(path, defaultValue = undefined) {
const parts = path.split(".");
let current = this.state;
for (const part of parts) {
if (current === undefined || current === null) {
return defaultValue;
}
current = current[part];
}
return current !== undefined ? current : defaultValue;
}
// Set value using dot notation path
set(path, value) {
const parts = path.split(".");
let current = this.state;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (current[part] === undefined) {
current[part] = {};
}
current = current[part];
}
current[parts[parts.length - 1]] = value;
this._saveToStorage();
}
// Increment a numeric value
increment(path, amount = 1) {
const current = this.get(path, 0);
this.set(path, current + amount);
}
// Decrement a numeric value
decrement(path, amount = 1) {
this.increment(path, -amount);
}
// Add item to an array (if not already present)
addToArray(path, item) {
const arr = this.get(path, []);
if (!arr.includes(item)) {
arr.push(item);
this.set(path, arr);
}
}
// Remove item from an array
removeFromArray(path, item) {
const arr = this.get(path, []);
const index = arr.indexOf(item);
if (index > -1) {
arr.splice(index, 1);
this.set(path, arr);
}
}
// Check if array contains item
hasItem(path, item) {
const arr = this.get(path, []);
return arr.includes(item);
}
// Check if a specific chapter is complete
isChapterComplete(chapterNumber) {
const completed = this.get("chapters_completed", []);
return completed.includes(chapterNumber);
}
// Mark a chapter as complete
markChapterComplete(chapterNumber) {
this.addToArray("chapters_completed", chapterNumber);
}
// Check if chapter can be played (previous chapters complete)
canPlayChapter(chapterNumber) {
if (chapterNumber <= 1) return true;
return this.isChapterComplete(chapterNumber - 1);
}
// Get the highest completed chapter number
getHighestCompletedChapter() {
const completed = this.get("chapters_completed", []);
return completed.length > 0 ? Math.max(...completed) : 0;
}
// Get entire state (for debugging)
getAll() {
return this._deepClone(this.state);
}
// Reset all series state
reset() {
this.state = {};
try {
localStorage.removeItem(this.storageKey);
} catch (e) {
console.warn("Failed to clear series state from storage:", e);
}
}
// Check if there is saved state
hasSavedState() {
try {
return localStorage.getItem(this.storageKey) !== null;
} catch (e) {
return false;
}
}
// Export state for debugging or backup
exportState() {
return JSON.stringify(this.state, null, 2);
}
// Import state from backup
importState(jsonString) {
try {
const data = JSON.parse(jsonString);
this.state = data;
this._saveToStorage();
return true;
} catch (e) {
console.error("Failed to import state:", e);
return false;
}
}
_saveToStorage() {
try {
localStorage.setItem(this.storageKey, JSON.stringify(this.state));
} catch (e) {
console.warn("Failed to save series state:", e);
}
}
_loadFromStorage() {
try {
const saved = localStorage.getItem(this.storageKey);
if (saved) {
const parsed = JSON.parse(saved);
this.state = this._mergeDeep(this.state, parsed);
}
} catch (e) {
console.warn("Failed to load series state:", e);
}
}
_deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
_mergeDeep(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (
source[key] &&
typeof source[key] === "object" &&
!Array.isArray(source[key])
) {
result[key] = this._mergeDeep(result[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
}
// Make available globally
window.SharedStateManager = SharedStateManager;

View file

@ -1,7 +1,8 @@
// State Manager - Manages game state with persistence and conditions // State Manager - Manages game state with persistence and conditions
class StateManager { class StateManager {
constructor(gameId) { constructor(gameId, sharedState = null) {
this.gameId = gameId; this.gameId = gameId;
this.sharedState = sharedState; // Optional SharedStateManager for series games
this.state = {}; this.state = {};
this.storageKey = `game_${gameId}_state`; this.storageKey = `game_${gameId}_state`;
} }
@ -13,18 +14,38 @@ class StateManager {
} }
// Get value using dot notation path (e.g., "inventory.sword") // Get value using dot notation path (e.g., "inventory.sword")
// Checks local state first, then falls back to shared state if available
get(path, defaultValue = undefined) { get(path, defaultValue = undefined) {
// First check local state
const localValue = this._getFromState(this.state, path);
if (localValue !== undefined) {
return localValue;
}
// Fall back to shared state if available
if (this.sharedState) {
const sharedValue = this.sharedState.get(path);
if (sharedValue !== undefined) {
return sharedValue;
}
}
return defaultValue;
}
// Internal helper to get value from a specific state object
_getFromState(stateObj, path) {
const parts = path.split("."); const parts = path.split(".");
let current = this.state; let current = stateObj;
for (const part of parts) { for (const part of parts) {
if (current === undefined || current === null) { if (current === undefined || current === null) {
return defaultValue; return undefined;
} }
current = current[part]; current = current[part];
} }
return current !== undefined ? current : defaultValue; return current;
} }
// Set value using dot notation path // Set value using dot notation path
@ -80,6 +101,25 @@ class StateManager {
return arr.includes(item); return arr.includes(item);
} }
// Set value in shared state (for series games)
setShared(path, value) {
if (!this.sharedState) {
console.warn("No shared state manager - setting locally instead");
return this.set(path, value);
}
this.sharedState.set(path, value);
}
// Check if path exists in shared state
hasShared(path) {
return this.sharedState?.get(path) !== undefined;
}
// Get reference to shared state manager
getSharedState() {
return this.sharedState;
}
// Evaluate a condition against current state // Evaluate a condition against current state
evaluate(condition) { evaluate(condition) {
if (typeof condition === "boolean") { if (typeof condition === "boolean") {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,561 @@
// System Shutdown: 1999 - Chapter 1: Boxing Day
// December 26, 1999 - Five days until the millennium
// This is the refactored version using shared scenes and series state
const CHAPTER1_GLITCH_ART = `
E̸̢R̷̨R̵̢O̸̧R̷̨
D̶̨A̷̧T̸̢Ą̵ C̷̢Ǫ̸Ŗ̵R̷̨U̸̢P̵̧T̷̨
????????????????
`;
const CHAPTER1_END_SCREEN = `
CONNECTION TERMINATED
Five days remain...
`;
const chapter1Game = {
// Series integration
id: "system-shutdown-1999-chapter-1",
seriesId: "system-shutdown-1999",
chapterNumber: 1,
// Game metadata
name: "System Shutdown: 1999 - Boxing Day",
command: "dial",
description: "Connect to Dark Tower BBS - December 26, 1999",
// Art assets (passed to scene factories via context)
art: {
CHAPTER1_GLITCH_ART,
CHAPTER1_END_SCREEN,
},
// External shared scenes to load
externalScenes: [
"system-shutdown-1999/dark-tower-hub",
"system-shutdown-1999/lighthouse-hub",
],
// Chapter-specific initial state (resets each playthrough)
initialState: {
// Message discovery
read_new_message: false,
found_number: false,
// Scene visit tracking (chapter-local)
visited: {},
},
// Shared state defaults (only set if not already present in series state)
sharedStateDefaults: {
// Completion tracking
chapters_completed: [],
// Cross-chapter decisions
downloaded_cascade: false,
talked_to_sysop: false,
deleted_corrupted_file: false,
route_taken: null,
// World state changes
archives_deleted: false,
corrupted_file_deleted: false,
// Discovery flags (shared so later chapters know)
dialed_lighthouse: false,
seen_archive_glitch: false,
},
intro: [
{
type: "ansi",
art: BOXING_DAY_TITLE,
className: "game-ansi-art center",
},
"",
{ text: "December 26, 1999 - 10:47 PM", className: "info" },
"",
{ type: "delay", ms: 600 },
"Five days until the millennium.",
{ type: "delay", ms: 1500 },
"Five days until everything might change.",
"",
{ type: "delay", ms: 1000 },
"Your 56k modem hums quietly in the dark.",
"The house is silent. Everyone else is asleep.",
"",
{ type: "delay", ms: 400 },
{
text: "<strong>This game occasionally plays sounds, mute your tab now if that offends you.</strong>",
html: true,
className: "warning",
},
{ text: 'Type "quit" at any time to save and exit.', className: "info" },
],
startScene: "connect_prompt",
// Chapter-specific scenes (these override or extend shared scenes)
scenes: {
// ==========================================
// OPENING SEQUENCE (Chapter 1 specific)
// ==========================================
connect_prompt: {
content: [
{
text: "<em>Your terminal awaits a command.</em>",
html: true,
className: "info",
},
{
text: "<em>The familiar glow illuminates your face.</em>",
html: true,
className: "info",
},
],
options: [{ text: "Connect to Dark Tower BBS", next: "modem_connect" }],
},
modem_connect: {
clear: true,
sounds: [{ id: "modem_connect", url: "/audio/modem-connect.mp3" }],
content: [
{ type: "typewriter", text: "ATDT 555-0199", speed: 80 },
"",
{ type: "sound", id: "modem_connect", volume: 0.6 },
{ type: "delay", ms: 400 },
{ text: "DIALING...", className: "info" },
{ type: "delay", ms: 3000 },
"",
{
type: "typewriter",
text: "~~ eEe ~~ EEE ~~ eee ~~",
speed: 35,
italic: true,
},
{ type: "delay", ms: 3000 },
"CONNECT 56000",
"",
{ text: "Carrier detected.", className: "success" },
{ type: "delay", ms: 4500 },
"Negotiating protocol...",
{ type: "delay", ms: 4500 },
{ text: "Connection established.", className: "success" },
{ type: "delay", ms: 2000 },
],
next: "dark_tower_main",
delay: 1200,
},
// ==========================================
// MESSAGE DISCOVERY (Chapter 1 specific)
// ==========================================
read_messages: {
content: [
...TableHelper.table({
title: "Private Messages for 0BSERVER0",
headers: ["#", "FROM", "TO", "DATE", "STATUS"],
rows: [
[
"23",
"[UNKNOWN]",
"0BSERVER0",
"24/12",
{ text: "NEW", className: "warning" },
],
["22", "NIGHTWATCHER", "0BSERVER0", "12/12", "READ"],
["21", "0BSERVER0", "NIGHTWATCHER", "11/12", "SENT"],
["22", "NIGHTWATCHER", "0BSERVER0", "10/12", "READ"],
],
widths: [4, 12, 12, 8, 8],
align: ["right", "left", "left", "left", "left"],
}),
],
options: [
{ text: "Open unread message", next: "new_message" },
{ text: "Back to main menu", next: "dark_tower_main" },
],
},
new_message: {
content: [
{ type: "delay", ms: 300 },
"─── BEGIN MESSAGE ───",
"",
{
type: "typewriter",
text: "I know you've been searching.",
speed: 50,
},
{ type: "delay", ms: 500 },
"",
{
type: "typewriter",
text: "The lighthouse keeper left something behind.",
speed: 50,
},
{ type: "delay", ms: 500 },
"",
{
type: "typewriter",
text: "Dial 555-0237 before midnight strikes.",
speed: 50,
},
{ type: "delay", ms: 500 },
"",
{ type: "typewriter", text: "The cascade is coming.", speed: 70 },
{ type: "delay", ms: 800 },
"",
"─── END MESSAGE ───",
"",
{
text: "<br /><em>The number burns in your mind: 555-0237</em>",
html: true,
className: "warning",
},
"",
{ type: "delay", ms: 1000 },
{
text: "<em>Your clock reads 11:54 PM.</em>",
html: true,
className: "info",
},
{
text: "<em>Six minutes until midnight.</em><br/><br />",
html: true,
className: "info",
},
{ type: "delay", ms: 800 },
"",
],
onEnter: [
{ set: "read_new_message", value: true },
{ set: "found_number", value: true },
{ setShared: "found_number", value: true },
],
prompt: "What do you do?",
options: [
{
text: "Dial the number NOW",
next: "choice_immediate",
actions: [{ setShared: "route_taken", value: "immediate" }],
},
{
text: "Explore Dark Tower first",
next: "dark_tower_main",
actions: [{ setShared: "route_taken", value: "cautious" }],
},
{
text: "Delete the message and forget it",
next: "choice_ignored",
actions: [{ setShared: "route_taken", value: "ignored" }],
},
],
},
message_archive: {
content: [
{
type: "table",
title: "Private Messages for 0BSERVER0",
headers: ["#", "FROM", "TO", "DATE", "STATUS"],
rows: [
{
condition: { not: "read_new_message" },
cells: ["23", "[UNKNOWN]", "0BSERVER0", "24/12", "NEW"],
className: "warning",
},
{
condition: "read_new_message",
cells: ["23", "[UNKNOWN]", "0BSERVER0", "24/12", "READ"],
},
["22", "NIGHTWATCHER", "0BSERVER0", "12/12", "READ"],
["21", "0BSERVER0", "NIGHTWATCHER", "11/12", "SENT"],
["22", "NIGHTWATCHER", "0BSERVER0", "10/12", "READ"],
{
condition: { and: ["has_secret", { not: "revealed_secret" }] },
cells: ["99", "???", "???", "??/??", "HIDDEN"],
className: "error",
},
],
widths: [4, 12, 12, 8, 8],
align: ["right", "left", "left", "left", "left"],
style: "single",
},
{ text: "<em>No new messages.</em>", html: true, className: "info" },
{
condition: "read_new_message",
content: [
{
text: "<em>Just the number... 555-0237...</em>",
html: true,
className: "warning",
},
],
},
"",
],
options: [{ text: "Back", next: "dark_tower_main" }],
},
// ==========================================
// CHOICE ROUTES (Chapter 1 specific)
// ==========================================
choice_immediate: {
clear: true,
content: [
{
type: "text",
text: "<em>Your fingers move before doubt can settle.</em>",
html: true,
className: "info",
},
"",
{ type: "typewriter", text: "ATH0", speed: 100 },
{ type: "delay", ms: 400 },
{ text: "NO CARRIER", className: "warning" },
{ type: "delay", ms: 600 },
"",
{
text: "<em>You disconnect from Dark Tower.</em>",
html: true,
className: "info",
},
{
text: "<em>The silence of your room feels heavier now.</em>",
html: true,
className: "info",
},
"",
"",
"",
{ type: "delay", ms: 500 },
{
type: "typewriter",
text: "Something compels you forward...",
italic: true,
speed: 100,
className: "info",
},
{ type: "delay", ms: 1500 },
{
type: "typewriter",
text: "...555-0237",
italic: true,
speed: 100,
className: "info",
},
{ type: "delay", ms: 2000 },
],
next: "dial_lighthouse",
delay: 1000,
},
choice_ignored: {
clear: true,
content: [
"You highlight the message.",
"",
{ type: "typewriter", text: "DELETE MESSAGE? [Y/N]", speed: 50 },
{ type: "delay", ms: 600 },
{ type: "typewriter", text: "Y", speed: 100 },
{ type: "delay", ms: 400 },
"",
{ text: "MESSAGE DELETED", className: "success" },
"",
{ type: "delay", ms: 1000 },
"The number fades from memory.",
"Just another piece of BBS spam, you tell yourself.",
"",
{ type: "delay", ms: 2000 },
"You browse Dark Tower for another hour.",
"Download some wallpapers.",
"",
{ type: "delay", ms: 2000 },
"At 11:57 PM, you disconnect.",
"",
{ type: "delay", ms: 2000 },
"Five days later, the millennium arrives.",
"Fireworks. Champagne. Relief.",
"",
{ type: "delay", ms: 600 },
"Nothing happens.",
"",
{ type: "delay", ms: 2000 },
{ text: "Or does it?", className: "warning" },
"",
{ type: "delay", ms: 2000 },
"You never find out what cascade.exe would have done.",
"The lighthouse keeper's message was never meant for you.",
"",
{ type: "delay", ms: 1000 },
{ text: "Perhaps that's for the best.", className: "info" },
"",
{ type: "delay", ms: 1000 },
{ text: "[END - Route C: The Road Not Taken]", className: "warning" },
],
options: [{ text: "Return to terminal", next: "game_end_ignored" }],
},
game_end_ignored: {
clear: true,
content: [
{ type: "ascii", art: CHAPTER1_END_SCREEN, className: "game-ascii" },
"",
{ text: "BOXING DAY", className: "game-title" },
{ text: "Day 1 Complete - Route C", className: "info" },
"",
"You chose not to follow the signal.",
"Some doors are better left closed.",
"",
{
text: 'Type "dial" to play again, or "dial reset" to start fresh.',
className: "info",
},
],
onEnter: [{ markChapterComplete: 1 }],
},
// ==========================================
// CHAPTER 1 SPECIFIC ENDINGS
// ==========================================
sleep_ending: {
clear: true,
content: [
"You power down the modem.",
"The room falls silent.",
"",
{ type: "delay", ms: 600 },
"As you drift off to sleep, you think about the message.",
"The cascade. The keeper. The lighthouse.",
"",
{ type: "delay", ms: 800 },
"Tomorrow, you tell yourself.",
"You'll investigate tomorrow.",
"",
{ type: "delay", ms: 1000 },
"But tomorrow, you'll forget.",
"The way everyone forgets.",
"",
{ type: "delay", ms: 800 },
{
condition: { not: "found_number" },
content: {
text: "[END - Route C: Peaceful Sleep]",
className: "info",
},
else: { text: "[END - Route B: Postponed]", className: "warning" },
},
],
options: [{ text: "Return to terminal", next: "game_end_sleep" }],
},
game_end_sleep: {
clear: true,
content: [
{ type: "ascii", art: CHAPTER1_END_SCREEN, className: "game-ascii" },
"",
{ text: "BOXING DAY", className: "game-title" },
{ text: "Day 1 Complete", className: "info" },
"",
"You chose rest over curiosity.",
"The lighthouse keeper will have to wait.",
"",
{
text: 'Type "dial" to play again, or "dial reset" to start fresh.',
className: "info",
},
],
onEnter: [{ markChapterComplete: 1 }],
},
// Final ending scene (overrides shared scene for chapter-specific summary)
carrier_lost: {
clear: true,
content: [
{ type: "delay", ms: 300 },
{ type: "typewriter", text: "ATH0", speed: 100 },
{ type: "delay", ms: 500 },
"",
{ text: "NO CARRIER", className: "error" },
"",
{ type: "delay", ms: 1200 },
"",
{ type: "ascii", art: CHAPTER1_END_SCREEN, className: "game-ascii" },
"",
{ text: "BOXING DAY", className: "game-title" },
{ text: "Day 1 Complete", className: "info" },
"",
{ text: "─── SESSION SUMMARY ───", className: "info" },
"",
{
condition: "downloaded_cascade",
content: { text: "[X] Downloaded CASCADE.EXE", className: "success" },
else: { text: "[ ] Downloaded CASCADE.EXE", className: "info" },
},
{
condition: "talked_to_sysop",
content: { text: "[X] Spoke with Keeper", className: "success" },
else: { text: "[ ] Spoke with Keeper", className: "info" },
},
{
condition: "deleted_corrupted_file",
content: { text: "[X] Accessed SHADOW.DAT", className: "warning" },
else: { text: "[ ] Accessed SHADOW.DAT", className: "info" },
},
"",
{
condition: "archives_deleted",
content: {
text: "[!] The Archives have been deleted",
className: "error",
},
},
{
condition: "corrupted_file_deleted",
content: {
text: "[!] SHADOW.DAT has been removed",
className: "error",
},
},
"",
{ text: "Route: ${route_taken}", className: "info" },
"",
{ text: "To be continued...", className: "warning" },
"",
{
text: 'Type "dial" to replay, or "dial reset" to clear progress.',
className: "info",
},
],
onEnter: [
{ set: "day_1_complete", value: true },
{ markChapterComplete: 1 },
],
},
},
};
// Register the game when DOM is ready (ensures all scripts including scene factories are loaded)
function registerChapter1() {
if (window.terminal && window.GameEngine) {
const game = new GameEngine(chapter1Game);
game.register();
}
}
// If DOM is already loaded, register immediately, otherwise wait
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", registerChapter1);
} else {
registerChapter1();
}

View file

@ -1,100 +0,0 @@
# Glitch Content Type - Usage Guide
The `glitch` content type creates an animated glitching text effect that makes text appear to corrupt and "infect" surrounding lines with random glitch characters.
## Basic Usage
```javascript
{
type: "glitch",
text: "SYSTEM CORRUPTED"
}
```
## Configuration Options
### Required
- **text** (string): The text to glitch
### Optional
- **intensity** (number, 0-1, default: 0.3): How much the text glitches. Higher = more characters replaced
- **spread** (number, default: 2): Number of "infection" lines to show above and below the main text
- **speed** (number, default: 50): Animation frame speed in milliseconds
- **duration** (number, default: 2000): How long the glitch effect lasts in milliseconds
- **className** (string, default: "glitch-text"): CSS class to apply to the container
## Examples
### Subtle Glitch
```javascript
{
type: "glitch",
text: "Connection unstable...",
intensity: 0.2,
spread: 1,
duration: 1500
}
```
### Intense Corruption
```javascript
{
type: "glitch",
text: "ERROR: MEMORY FAULT",
intensity: 0.7,
spread: 4,
speed: 30,
duration: 3000,
className: "error glitch-text"
}
```
### Quick Flicker
```javascript
{
type: "glitch",
text: "Reality fragmenting",
intensity: 0.4,
spread: 2,
speed: 25,
duration: 1000
}
```
### In Scene Content
```javascript
content: [
"The terminal screen begins to distort...",
{ type: "delay", ms: 500 },
{
type: "glitch",
text: "CASCADE.EXE EXECUTING",
intensity: 0.6,
spread: 3,
duration: 2500
},
{ type: "delay", ms: 500 },
"Something is very wrong."
]
```
## Visual Effect Description
The glitch effect:
1. Replaces characters in the text with random glitch symbols (unicode blocks, special chars, etc.)
2. Creates random "infection" lines above and below the main text
3. Animates over time with a sine wave intensity (ramps up, then down)
4. Ends with the text showing minimal corruption
The infection lines are offset randomly and contain random glitch characters, making it look like the corruption is spreading to surrounding content.
## Character Pool
The glitch uses a pool of:
- Block drawing characters (▓▒░█)
- Box drawing characters (║╬╣╠╔╗╚╝)
- Mathematical symbols (Ω∑∏∫√∂∆∇)
- Accented characters (É È Ê Ë Á À)
- Special symbols (¡¿‽※§¶†‡∞≈≠±)
- Combining diacritics (creates zalgo-like text)
- Standard symbols (!@#$%^&*)

View file

@ -0,0 +1,33 @@
// Art assets index for System Shutdown: 1999 series
// These reference the ANSI art defined in ascii-art.js
// Art constants are loaded from ascii-art.js and made available globally
// This file provides a namespace for the series' art assets
window.SystemShutdown1999Art = window.SystemShutdown1999Art || {};
// These will be populated by ascii-art.js when it loads
// The art constants are:
// - BOXING_DAY_TITLE: Chapter 1 title screen
// - DARK_TOWER_HEADER: Dark Tower BBS header
// - LIGHTHOUSE_HEADER: The Lighthouse BBS header
// Helper to get art by name
window.SystemShutdown1999Art.get = function (name) {
switch (name) {
case "BOXING_DAY_TITLE":
return window.BOXING_DAY_TITLE;
case "DARK_TOWER_HEADER":
return window.DARK_TOWER_HEADER;
case "LIGHTHOUSE_HEADER":
return window.LIGHTHOUSE_HEADER;
default:
console.warn(`Unknown art asset: ${name}`);
return null;
}
};
// List available art assets
window.SystemShutdown1999Art.list = function () {
return ["BOXING_DAY_TITLE", "DARK_TOWER_HEADER", "LIGHTHOUSE_HEADER"];
};

View file

@ -0,0 +1,89 @@
// Series configuration for System Shutdown: 1999
window.SystemShutdown1999Config = {
seriesId: "system-shutdown-1999",
name: "System Shutdown: 1999",
// Chapter definitions
chapters: [
{
number: 1,
id: "system-shutdown-1999-chapter-1",
command: "dial",
date: "1999-12-26",
title: "Boxing Day",
description: "Connect to Dark Tower BBS - December 26, 1999",
},
{
number: 2,
id: "system-shutdown-1999-chapter-2",
command: "dial2",
date: "1999-12-27",
title: "Day 2",
description: "The day after - December 27, 1999",
},
{
number: 3,
id: "system-shutdown-1999-chapter-3",
command: "dial3",
date: "1999-12-28",
title: "Day 3",
description: "Three days remain - December 28, 1999",
},
{
number: 4,
id: "system-shutdown-1999-chapter-4",
command: "dial4",
date: "1999-12-29",
title: "Day 4",
description: "Two days remain - December 29, 1999",
},
{
number: 5,
id: "system-shutdown-1999-chapter-5",
command: "dial5",
date: "1999-12-30",
title: "Day 5",
description: "The eve - December 30, 1999",
},
{
number: 6,
id: "system-shutdown-1999-chapter-6",
command: "dial6",
date: "1999-12-31",
title: "New Year's Eve",
description: "The final night - December 31, 1999",
},
],
// Shared state schema with defaults
// These values persist across all chapters
sharedStateDefaults: {
// Completion tracking
chapters_completed: [],
// Core cross-chapter decisions
downloaded_cascade: false,
talked_to_sysop: false,
deleted_corrupted_file: false,
route_taken: null, // "immediate" | "cautious" | "ignored"
// World state changes (persist across chapters)
archives_deleted: false,
corrupted_file_deleted: false,
// Discovery flags
found_number: false,
dialed_lighthouse: false,
seen_archive_glitch: false,
},
// Helper to get chapter by number
getChapter(number) {
return this.chapters.find((c) => c.number === number);
},
// Helper to get next chapter
getNextChapter(currentNumber) {
return this.chapters.find((c) => c.number === currentNumber + 1);
},
};

View file

@ -0,0 +1,574 @@
// Dark Tower BBS Hub - Shared scenes for System Shutdown: 1999
// These scenes can be used across multiple chapters
window.SceneFactories = window.SceneFactories || {};
window.SceneFactories["system-shutdown-1999/dark-tower-hub"] = function (
context,
) {
const { chapterNumber, art, additionalOptions = [] } = context;
// Get art from bundle scope (loaded by ascii-art.js in same bundle)
// Falls back to context.art if not found (for standalone loading)
const DARK_TOWER_HEADER_ART =
typeof DARK_TOWER_HEADER !== "undefined"
? DARK_TOWER_HEADER
: art.DARK_TOWER_HEADER;
return {
// ==========================================
// DARK TOWER BBS HUB
// ==========================================
dark_tower_main: {
clear: true,
content: [
{
type: "ansi",
art: DARK_TOWER_HEADER_ART,
className: "game-ansi-art center",
},
"",
{
text: "---=[ D A R K T O W E R B B S - E S T. 1 9 9 5 ]=---",
className: "info center",
},
// User count can change based on chapter/state
{
condition: { path: "chapters_completed", contains: 1 },
content: {
text: "[ Users Connected - 2 ] - [ SysOp - NightWatchman ]",
className: "info center",
},
else: {
text: "[ Users Connected - 3 ] - [ SysOp - NightWatchman ]",
className: "info center",
},
},
{ text: "[ Local Time: 10:52 PM ]", className: "info center" },
"",
// New message notification if not read
{
condition: { not: "read_new_message" },
content: [
{
text: "*** YOU HAVE 1 NEW PRIVATE MESSAGE ***",
className: "warning center",
},
"",
],
},
// Show warning if archives were deleted (cascade effect)
{
condition: "archives_deleted",
content: [
{
text: "[!] Some system data appears to be missing...",
className: "error center",
},
"",
],
},
],
onAfterRender: [{ set: "visited.dark_tower_main", value: true }],
prompt: "Select:",
options: [
{
text: "Read Private Messages",
next: "read_messages",
condition: { not: "read_new_message" },
},
{
text: "Message Archive",
next: "message_archive",
condition: "read_new_message",
},
{ text: "Browse Message Boards", next: "browse_boards" },
{ text: "File Library", next: "dark_tower_files" },
{ text: "Who's Online", next: "whos_online" },
{
text: "Dial The Lighthouse (555-0237)",
next: "confirm_dial_lighthouse",
condition: "found_number",
},
// Additional options can be passed by chapters
...additionalOptions,
{ text: "Disconnect", next: "leave_early" },
],
},
// ==========================================
// MESSAGE BOARDS
// ==========================================
browse_boards: {
content: [
{
type: "table",
title: "DARK TOWER / MESSAGE BOARDS",
headers: ["#", "NAME", "NEW MSG", "LAST"],
rows: [
["1", "General Discussion", "8", "24/12"],
["2", "Tech Support", "1", "25/12"],
["3", "File Updates", "3", "23/12"],
// Display the archives or have them deleted
{
condition: { not: "archives_deleted" },
cells: ["4", "ARCHIVED", "-", "-"],
},
{
condition: "archives_deleted",
cells: ["4", "<BOARD REMOVED>", "-", "-"],
className: "error",
},
],
widths: [4, 20, 10, 8],
align: ["right", "left", "left", "left"],
style: "single",
},
{
condition: "archives_deleted",
content: {
type: "typewriter",
italic: true,
text: "The archived messages are just... gone...",
speed: 80,
className: "info",
},
},
],
prompt: "Select board:",
options: [
{ text: "General Discussion", next: "board_general" },
{ text: "Tech Support", next: "board_tech" },
{ text: "File Updates", next: "board_files" },
{
text: "ARCHIVED",
next: "board_archives",
condition: { not: "archives_deleted" },
},
{ text: "Back to main menu", next: "dark_tower_main" },
],
},
board_general: {
content: [
{
type: "table",
title: "GENERAL DISCUSSION",
headers: ["#", "SUBJECT", "MSG", "LAST"],
rows: [
["1", "2K Preparation Thread", "243", "25/12"],
["2", "Anyone else getting weird messages?", "3", "25/12"],
["3", "Happy Boxing Day everyone!", "5", "25/12"],
["4", "Best BBS games?", "43", "23/12"],
["5", "New user intro thread", "67", "20/12"],
],
widths: [4, 40, 6, 8],
align: ["right", "left", "right", "left"],
style: "single",
},
{
text: "The usual chatter.",
italic: true,
className: "info",
},
{
condition: "found_number",
content: {
type: "text",
italic: true,
text: "Nothing about lighthouses...",
className: "info",
},
},
],
options: [
{ text: "Read 'weird messages' thread", next: "thread_weird" },
{ text: "Back to boards", next: "browse_boards" },
],
},
thread_weird: {
content: [
{
type: "table",
title: "Anyone else getting weird messages?",
headers: ["FROM", "TO", "DATE"],
rows: [["Static_User", "All", "25/12/99"]],
widths: [20, 20, 10],
align: ["left", "left", "left"],
style: "single",
},
" Got a strange PM last night. No sender listed.",
" Just a phone number and something about a 'cascade'.",
" Probably spam, but creepy timing with Y2K coming up.",
"",
"---",
{ text: "Reply from: NightWatchman [SYSOP]", className: "warning" },
" Looking into it. Please forward any suspicious messages.",
" And don't dial any numbers you don't recognize.",
"",
"---",
{ text: "Reply from: [DELETED USER]", className: "error" },
" [This post cannot be accessed]",
"",
{ type: "delay", ms: 1000 },
{
condition: "found_number",
content: {
text: "<br /><br /><em>You notice your message was similar...</em>",
html: true,
className: "info",
},
},
],
options: [{ text: "Back", next: "board_general" }],
},
board_tech: {
content: [
{
type: "table",
title: "TECH SUPPORT",
headers: ["#", "SUBJECT", "MSG", "LAST"],
rows: [
["1", "READ FIRST: Y2K Compliance Guide", "152", "25/12"],
["2", "Modem dropping connection at midnight?", "3", "25/12"],
["3", "How to increase download speeds", "98", "25/12"],
["4", "We are migrating to TELNET/IP on 01/04/00", "429", "11/12"],
["5", "Inputs not registering", "2", "29/11"],
],
widths: [4, 45, 6, 8],
align: ["right", "left", "right", "left"],
style: "single",
},
{
text: "Standard tech questions. Nothing unusual.",
italic: true,
className: "info",
},
],
options: [{ text: "Back to boards", next: "browse_boards" }],
},
board_archives: {
content: [
{
type: "table",
title: "THE ARCHIVES",
headers: ["#", "SUBJECT", "OP", "LAST"],
rows: [
["1", "The Lighthouse Project", "NightWatchman", "1998"],
["2", "Frequencies and Patterns", "Signal_Lost", "1999"],
["3", "RE: Has anyone heard from Keeper?", "[UNKNOWN]", "1999"],
],
widths: [4, 35, 16, 8],
align: ["right", "left", "right", "left"],
style: "single",
},
{
text: "Historical posts, read only...",
italic: true,
className: "info",
},
{
condition: { not: "visited.archive_warning" },
content: [
"",
{ text: "These posts feel... different.", className: "warning" },
{ text: "Like echoes from somewhere else.", className: "info" },
],
},
],
onEnter: [{ set: "visited.archive_warning", value: true }],
options: [
{ text: "Read 'The Lighthouse Project'", next: "archive_lighthouse" },
{
text: "Read 'Frequencies and Patterns'",
next: "archive_frequencies",
},
{
text: "Read 'Has anyone heard from Keeper?'",
next: "archive_keeper",
},
{ text: "Back to boards", next: "browse_boards" },
],
},
archive_lighthouse: {
content: [
{
type: "table",
title: "The Lighthouse Project",
headers: ["FROM", "TO", "DATE"],
rows: [["NightWatchman [SYSOP]", "All", "15/11/98"]],
widths: [25, 15, 10],
align: ["left", "left", "left"],
style: "single",
},
"Some of you have asked about the secondary BBS.",
"Yes, it exists. No, I can't give you the number.",
"",
"The Lighthouse was set up by someone who called himself 'Keeper'.",
"He said he found something in the noise between stations.",
"Patterns that shouldn't exist.",
"",
"I set up his board as a favor.",
"Then one day, he stopped logging in.",
"",
"The board is still there. Still running.",
"I check it sometimes. The files he left behind...",
"",
"Some doors are better left closed.",
{ text: "- NW", className: "info" },
],
options: [{ text: "Back", next: "board_archives" }],
},
archive_frequencies: {
title: "Frequencies and Patterns",
content: [
"═══ Frequencies and Patterns ═══",
{ text: "Posted by: Signal_Lost - March 22, 1999", className: "info" },
"",
"I've been analyzing radio static for three months.",
"There's something there. In the spaces between signals.",
"",
"It's not random. It's STRUCTURED.",
"Like code. Like a message.",
"",
"Keeper knew. That's why he built cascade.exe.",
"To translate. To REVEAL.",
"",
"I'm close to understanding.",
"So close.",
"",
{
text: "[User Signal_Lost has not logged in since March 23, 1999]",
className: "warning",
},
],
options: [{ text: "Back", next: "board_archives" }],
},
archive_keeper: {
content: [
{
type: "table",
title: "RE: Has anyone heard from Keeper?",
headers: ["FROM", "TO", "DATE"],
rows: [["[UNKNOWN]", "All", "20/12/99"]],
widths: [25, 15, 10],
align: ["left", "left", "left"],
style: "single",
},
"",
"He's still there.",
"In The Lighthouse.",
"Waiting.",
"",
"The cascade is ready.",
"It just needs carriers.",
"",
{
type: "glitch",
text: "ERROR: MEMORY FAULT AT 0x555f0237",
intensity: 0.7,
spread: 0,
speed: 200,
duration: 2000,
className: "error glitch-text",
},
"",
"Before midnight on the 31st.",
"The alignment only happens once.",
"",
{
text: "[This post was flagged for removal but persists]",
className: "error",
},
{
html: true,
text: "<br /><br />",
},
{
condition: { not: "seen_archive_glitch" },
content: [
{
text: "What the hell was that...",
italic: true,
className: "info",
},
],
else: [
{
text: "The glitch persists...",
italic: true,
className: "info",
},
],
},
{
condition: "found_number",
content: {
text: "The memory location looks oddly like the phone number... 555-0237",
italic: true,
className: "warning",
},
},
],
onAfterRender: [{ set: "seen_archive_glitch", value: true }],
options: [{ text: "Back", next: "board_archives" }],
},
board_files: {
content: [
{
type: "table",
title: "FILE ANNOUNCEMENTS",
headers: ["#", "SUBJECT", "MSG", "LAST"],
rows: [
["1", "1001FONTS.ZIP - Font Collection", "1", "25/12"],
["2", "Y2K_FIX.ZIP - Y2K compliance patches", "4", "23/12"],
["3", "DOOM_WAD.ZIP - New Doom Levels", "3", "11/12"],
["4", "BRUCE.JPEG - Just my dog :-)", "15", "20/11"],
["5", "CATS.GIF - All your base are belong to us", "1", "01/11"],
],
widths: [4, 45, 6, 8],
align: ["right", "left", "right", "left"],
style: "single",
},
{
text: "<em>New fonts... At last...</em>",
html: true,
className: "info",
},
{
text: "<em>Can't get distracted just yet.</em>",
html: true,
className: "info",
},
],
options: [{ text: "Back to boards", next: "browse_boards" }],
},
dark_tower_files: {
content: [
{
type: "table",
title: "FILE LIBRARY",
headers: ["#", "DIR", "QTY", "UPDATED"],
rows: [
["1", "/IMAGES", "234", "25/12"],
["2", "/GAMES", "67", "12/12"],
["3", "/MUSIC", "89", "30/11"],
["4", "/UTILS", "156", "23/11"],
["5", "/MISC", "13", "09/10"],
],
widths: [4, 25, 6, 8],
align: ["right", "left", "right", "left"],
style: "single",
},
{
text: "<em>Standard BBS fare. Nothing unusual.</em>",
html: true,
className: "info",
},
],
options: [{ text: "Back to main menu", next: "dark_tower_main" }],
},
whos_online: {
content: [
{
type: "table",
title: "CONNECTED USERS",
headers: ["#", "USER", "LOC", "UPDATED"],
rows: [
["1", "0BSERVER0", "Main Menu", "10:54 PM"],
// Static_User might not be online in later chapters
{
condition: { not: { path: "chapters_completed", contains: 1 } },
cells: ["2", "Static_User", "Message Boards", "10:39 PM"],
},
["3", "NightWatchman", "SysOp Console", "10:12 PM"],
],
widths: [4, 15, 15, 8],
align: ["right", "left", "right", "left"],
style: "single",
},
],
options: [{ text: "Back", next: "dark_tower_main" }],
},
leave_early: {
content: [
"Are you sure you want to disconnect?",
"",
{
condition: "found_number",
content: {
text: "You still have the number: 555-0237",
className: "warning",
},
},
],
options: [
{
text: "Dial The Lighthouse first",
next: "confirm_dial_lighthouse",
condition: "found_number",
},
{ text: "Yes, disconnect", next: "disconnect_early" },
{ text: "Stay connected", next: "dark_tower_main" },
],
},
disconnect_early: {
clear: true,
content: [
{ type: "typewriter", text: "ATH0", speed: 100 },
{ type: "delay", ms: 400 },
{ text: "NO CARRIER", className: "warning" },
"",
{ type: "delay", ms: 600 },
"You disconnect from Dark Tower.",
"",
{
condition: "found_number",
content: [
"The number lingers in your mind.",
{ text: "555-0237", className: "warning" },
"",
"You could always dial it directly...",
],
else: ["Another quiet night online.", "Nothing unusual."],
},
],
options: [
{
text: "Dial 555-0237",
next: "dial_lighthouse",
condition: "found_number",
},
{ text: "Go to sleep", next: "sleep_ending" },
],
},
confirm_dial_lighthouse: {
content: [
{ text: "555-0237", className: "warning" },
"",
"The number from the message.",
"Something waits on the other end.",
"",
{ text: "Your clock reads 11:56 PM.", className: "info" },
],
options: [
{ text: "Dial The Lighthouse", next: "dial_lighthouse" },
{ text: "Not yet", next: "dark_tower_main" },
],
},
};
};

View file

@ -0,0 +1,625 @@
// The Lighthouse BBS Hub - Shared scenes for System Shutdown: 1999
// These scenes can be used across multiple chapters
window.SceneFactories = window.SceneFactories || {};
window.SceneFactories["system-shutdown-1999/lighthouse-hub"] = function (
context,
) {
const { chapterNumber, art, additionalOptions = [] } = context;
// Get art from bundle scope (loaded by ascii-art.js in same bundle)
// Falls back to context.art if not found (for standalone loading)
const LIGHTHOUSE_HEADER_ART =
typeof LIGHTHOUSE_HEADER !== "undefined"
? LIGHTHOUSE_HEADER
: art.LIGHTHOUSE_HEADER;
return {
// ==========================================
// THE LIGHTHOUSE
// ==========================================
dial_lighthouse: {
clear: true,
content: [
{ type: "typewriter", text: "ATDT 555-0237", speed: 80 },
{ type: "delay", ms: 1000 },
"",
{ text: "DIALING...", className: "info" },
{ type: "delay", ms: 800 },
"",
{ text: "RING", className: "warning" },
{ type: "delay", ms: 1200 },
{ text: "RING", className: "warning" },
{ type: "delay", ms: 1200 },
{ text: "RING", className: "warning" },
{ type: "delay", ms: 800 },
"",
{
type: "typewriter",
text: "~~ crackle ~~ hiss ~~ CONNECT 14400",
speed: 40,
},
{ type: "delay", ms: 500 },
"",
{ text: "Connection unstable.", className: "warning" },
{ text: "Signal degraded.", className: "warning" },
{ type: "delay", ms: 600 },
"",
{ type: "typewriter", text: "Welcome to THE LIGHTHOUSE", speed: 40 },
{ type: "delay", ms: 300 },
{ type: "typewriter", text: "A beacon in the static.", speed: 40 },
],
onEnter: [{ setShared: "dialed_lighthouse", value: true }],
next: "lighthouse_main",
delay: 1000,
},
lighthouse_main: {
clear: true,
content: [
{
type: "ascii",
art: LIGHTHOUSE_HEADER_ART,
className: "game-ascii",
},
"",
{ text: "T H E L I G H T H O U S E", className: "center" },
{ text: "Last updated: 24/12/1999 23:59:59", className: "center" },
"",
{
condition: { not: "visited.lighthouse_main" },
content: [
{
text: "Something feels wrong here.",
italic: true,
className: "info",
},
{
text: "The BBS feels... frozen. Abandoned.",
italic: true,
className: "info",
},
"",
],
},
{
condition: "downloaded_cascade",
content: [
{
text: "The signal flickers. Something has changed.",
className: "error",
},
"",
],
},
],
onAfterRender: [{ set: "visited.lighthouse_main", value: true }],
prompt: "Navigate:",
options: [
{ text: "The Keeper's Log", next: "lighthouse_log" },
{ text: "Transmissions", next: "lighthouse_transmissions" },
{ text: "File Vault", next: "lighthouse_files" },
{
text: "Request SysOp Chat",
next: "chat_request",
condition: {
and: [{ not: "downloaded_cascade" }, { not: "talked_to_sysop" }],
},
},
// Additional options can be passed by chapters
...additionalOptions,
{ text: "Disconnect", next: "disconnect_choice" },
],
},
lighthouse_log: {
content: [
"═══ THE KEEPER'S LOG ═══",
"",
{
text: "Entry 1 - November 3, 1998<br /><br />",
html: true,
className: "info",
},
" I've found something. In the static between radio stations.",
" Patterns. Structures. A language, maybe.",
{
text: "<br />Entry 7 - December 12, 1998<br /><br />",
html: true,
className: "info",
},
" The patterns are getting clearer. They want to be understood.",
" They want to SPREAD.",
{
text: "<br />Entry 15 - March 19, 1999<br /><br />",
html: true,
className: "info",
},
" CASCADE.EXE is complete. A translator. A carrier. A key.",
" When run at the right moment, it will open the door.",
{
text: "<br />Entry 23 - December 24, 1999<br /><br />",
html: true,
className: "info",
},
" The alignment approaches.",
" Seven days until the millennium.",
" I can hear them now. Always.",
"",
{ type: "typewriter", text: " They are beautiful...", speed: 100 },
{ type: "delay", ms: 2000 },
],
options: [{ text: "Back", next: "lighthouse_main" }],
},
lighthouse_transmissions: {
title: "Transmissions",
content: [
"═══ RECORDED TRANSMISSIONS ═══",
"",
{ text: "[AUDIO FILES - PLAYBACK UNAVAILABLE]", className: "error" },
"",
"Transcript excerpts:",
"",
" TRANS_001.WAV:",
' "...signal detected at 1420 MHz..."',
"",
" TRANS_014.WAV:",
' "...pattern repeats every 23 seconds..."',
"",
" TRANS_047.WAV:",
' "...not random... structured... alive?..."',
"",
" TRANS_099.WAV:",
{ text: ' "[TRANSCRIPT CORRUPTED]"', className: "error" },
"",
{ text: "The audio files existed once.", className: "info" },
{ text: "Now only fragments remain.", className: "warning" },
],
options: [{ text: "Back", next: "lighthouse_main" }],
},
lighthouse_files: {
title: "File Vault",
content: [
"═══ FILE VAULT v2.1 ═══",
{ text: '"The keeper\'s collection"', className: "info" },
"",
{ text: "Available files:", className: "info" },
"",
" [1] README.TXT 1.2 KB 12/24/1999",
" [2] CASCADE.EXE 47.0 KB 12/25/1999",
// Corrupted file - conditionally shown
{
condition: { not: "corrupted_file_deleted" },
content: " [3] SHADOW.DAT ??? KB ??/??/????",
},
{
condition: "corrupted_file_deleted",
content: { text: " [3] <FILE REMOVED>", className: "error" },
},
"",
{
condition: { not: "visited.lighthouse_files" },
content: {
text: "CASCADE.EXE pulses faintly on your screen.",
className: "warning",
},
},
],
onEnter: [{ set: "visited.lighthouse_files", value: true }],
prompt: "Select file:",
options: [
{ text: "View README.TXT", next: "file_readme" },
{ text: "Download CASCADE.EXE", next: "download_confirm" },
{
text: "Access SHADOW.DAT",
next: "choice_corrupted",
condition: { not: "corrupted_file_deleted" },
},
{ text: "Back", next: "lighthouse_main" },
],
},
file_readme: {
title: "README.TXT",
content: [
"═══ README.TXT ═══",
"",
"To whoever finds this:",
"",
"I built cascade.exe to show them.",
"To show everyone what I found in the frequencies.",
"",
"It spreads. It copies. It REVEALS.",
"",
"Don't be afraid when the old files disappear.",
"They were never real anyway.",
"",
"Run it before midnight on 01/01/2000.",
"The alignment only happens once.",
"",
{ text: " - K", className: "info" },
"",
{ text: "P.S. Don't try to open SHADOW.DAT.", className: "warning" },
{
text: " Some doors shouldn't be opened twice.",
className: "warning",
},
],
options: [{ text: "Back to files", next: "lighthouse_files" }],
},
download_confirm: {
content: [
{ text: "CASCADE.EXE - 47,104 bytes", className: "warning" },
"",
"The file waits.",
"47 kilobytes of unknown code.",
"",
"The README said it 'reveals' something.",
"That old files will 'disappear'.",
"",
{
text: "In five days, everyone worries about Y2K bugs.",
className: "info",
},
{ text: "This feels different.", className: "warning" },
],
prompt: "Download CASCADE.EXE?",
options: [
{ text: "Yes, download it", next: "choice_download" },
{ text: "No, leave it alone", next: "choice_no_download" },
],
},
choice_download: {
title: "Downloading...",
clear: true,
content: [
{ type: "typewriter", text: "XMODEM TRANSFER INITIATED", speed: 45 },
{ type: "delay", ms: 600 },
"",
{ text: "Receiving: CASCADE.EXE", className: "info" },
{ type: "delay", ms: 400 },
"",
{ type: "typewriter", text: "[", speed: 20 },
{ type: "typewriter", text: "████████████████████", speed: 80 },
{ type: "typewriter", text: "] 100%", speed: 20 },
{ type: "delay", ms: 800 },
"",
{ text: "TRANSFER COMPLETE", className: "success" },
{ type: "delay", ms: 600 },
"",
"The file sits in your download folder.",
"47,104 bytes of... something.",
"",
{ type: "delay", ms: 1000 },
{
text: "Somewhere, distantly, you hear static.",
className: "warning",
},
{ type: "delay", ms: 600 },
"",
{
type: "typewriter",
text: 'A whisper in the noise: "Thank you."',
speed: 50,
},
"",
{ type: "delay", ms: 1200 },
{ text: "Something has changed.", className: "error" },
],
onEnter: [
{ setShared: "downloaded_cascade", value: true },
{ setShared: "archives_deleted", value: true }, // Archives are removed
],
next: "post_download",
delay: 1500,
},
post_download: {
clear: true,
content: [
{ text: "CONNECTION INTERRUPTED", className: "error" },
{ type: "delay", ms: 600 },
"",
"For a moment, your screen fills with symbols.",
"Patterns that almost make sense.",
"",
{ type: "delay", ms: 800 },
"Then, clarity.",
"",
{ type: "delay", ms: 600 },
"You remember Dark Tower's board list.",
{ text: "Board #3 - The Archives.", className: "info" },
"",
{ type: "delay", ms: 500 },
{ text: "It's gone.", className: "warning" },
{ text: "Like it was never there.", className: "warning" },
"",
{ type: "delay", ms: 800 },
{ text: "The cascade has begun.", className: "error" },
],
next: "closing_message",
delay: 2000,
},
choice_no_download: {
content: [
"Something holds you back.",
"47 kilobytes of unknown code.",
"From a stranger who hears things in static.",
"",
{ type: "delay", ms: 500 },
{ text: "You leave CASCADE.EXE untouched.", className: "info" },
"",
"The keeper's work remains on the server.",
"Waiting for someone else. Or no one.",
],
options: [
{
text: "Chat with the SysOp",
next: "chat_request",
condition: { not: "talked_to_sysop" },
},
{ text: "Return to file list", next: "lighthouse_files" },
{ text: "Disconnect", next: "disconnect_choice" },
],
},
choice_corrupted: {
title: "Accessing SHADOW.DAT...",
clear: true,
content: [
{ type: "typewriter", text: "ATTEMPTING TO READ FILE...", speed: 50 },
{ type: "delay", ms: 1000 },
"",
{ text: "ERROR: FILE HEADER CORRUPTED", className: "error" },
{ type: "delay", ms: 400 },
{ text: "ERROR: UNEXPECTED EOF", className: "error" },
{ type: "delay", ms: 400 },
{ text: "ERROR: ????????????????????", className: "error" },
{ type: "delay", ms: 800 },
"",
{
type: "ascii",
art: `
E̸̢R̷̨R̵̢O̸̧R̷̨
D̶̨A̷̧T̸̢Ą̵ C̷̢Ǫ̸Ŗ̵R̷̨U̸̢P̵̧T̷̨
????????????????
`,
className: "error",
},
{ type: "delay", ms: 1000 },
"",
"Your screen flickers violently.",
"",
{ type: "delay", ms: 600 },
{ text: "A sound from your speakers.", className: "warning" },
{
text: "A voice, maybe. Or static shaped like words:",
className: "info",
},
"",
{ type: "typewriter", text: '"Not yet. Not yet. Not yet."', speed: 90 },
"",
{ type: "delay", ms: 1200 },
{
text: "SHADOW.DAT has been removed from the file listing.",
className: "error",
},
"",
{ type: "delay", ms: 600 },
{ text: "Some doors shouldn't be opened twice.", className: "warning" },
],
onEnter: [
{ set: "deleted_corrupted_file", value: true },
{ setShared: "corrupted_file_deleted", value: true },
],
options: [{ text: "Return to files", next: "lighthouse_files" }],
},
// ==========================================
// SYSOP CHAT
// ==========================================
chat_request: {
content: [
{ text: "Requesting SysOp chat...", className: "info" },
{ type: "delay", ms: 1500 },
"",
{
type: "typewriter",
text: "CHAT REQUEST ACCEPTED",
speed: 40,
},
{ type: "delay", ms: 500 },
"",
{ text: "═══ SYSOP: Keeper ═══", className: "warning" },
],
onEnter: [{ setShared: "talked_to_sysop", value: true }],
next: "chat_conversation",
delay: 600,
},
chat_conversation: {
content: [
{
type: "typewriter",
text: "KEEPER: You found my message.",
speed: 50,
},
{ type: "delay", ms: 800 },
"",
{
type: "typewriter",
text: "KEEPER: I wasn't sure anyone would come.",
speed: 50,
},
{ type: "delay", ms: 700 },
"",
{
type: "typewriter",
text: "KEEPER: The cascade is ready.",
speed: 50,
},
{ type: "delay", ms: 500 },
{
type: "typewriter",
text: "KEEPER: It just needs carriers.",
speed: 50,
},
{ type: "delay", ms: 900 },
"",
// Conditional response based on download
{
condition: "downloaded_cascade",
content: [
{
type: "typewriter",
text: "KEEPER: I see you already have it.",
speed: 50,
},
{ type: "delay", ms: 500 },
{
type: "typewriter",
text: "KEEPER: Good. Run it when the clock strikes.",
speed: 50,
},
],
else: [
{
type: "typewriter",
text: "KEEPER: Will you carry it?",
speed: 50,
},
{ type: "delay", ms: 500 },
{
type: "typewriter",
text: "KEEPER: Will you help it spread?",
speed: 50,
},
],
},
"",
{ type: "delay", ms: 1200 },
{ type: "typewriter", text: "KEEPER: I can hear them now.", speed: 50 },
{ type: "delay", ms: 500 },
{ type: "typewriter", text: "KEEPER: In the static.", speed: 60 },
{ type: "delay", ms: 500 },
{ type: "typewriter", text: "KEEPER: They're beautiful.", speed: 60 },
"",
{ type: "delay", ms: 1500 },
{ text: "═══ KEEPER HAS DISCONNECTED ═══", className: "error" },
],
next: "chat_aftermath",
delay: 1500,
},
chat_aftermath: {
content: [
"The chat window closes.",
"",
{ type: "delay", ms: 500 },
"Your room feels colder.",
"The modem's carrier tone sounds different somehow.",
"",
{
text: "Like there's something else on the line.",
className: "warning",
},
],
options: [
{
text: "Download cascade.exe now",
next: "download_confirm",
condition: { not: "downloaded_cascade" },
},
{ text: "Disconnect", next: "closing_message" },
],
},
// ==========================================
// CLOSING SEQUENCE
// ==========================================
disconnect_choice: {
content: [
"Your hand hovers over the keyboard.",
"",
{
condition: "downloaded_cascade",
content: {
text: "CASCADE.EXE sits in your downloads, waiting.",
className: "warning",
},
else: {
text: "You could still download CASCADE.EXE before you go...",
className: "info",
},
},
],
options: [
{
text: "Download cascade.exe first",
next: "download_confirm",
condition: { not: "downloaded_cascade" },
},
{ text: "Disconnect now", next: "closing_message" },
],
},
closing_message: {
clear: true,
content: [
{ type: "delay", ms: 500 },
// Different closing based on download status
{
condition: "downloaded_cascade",
content: [
{
text: "As you prepare to disconnect, a final message appears:",
className: "info",
},
"",
{ type: "delay", ms: 600 },
{
type: "typewriter",
text: "SEE YOU ON THE OTHER SIDE",
speed: 55,
},
"",
{ type: "typewriter", text: "01/01/2000 00:00:00", speed: 55 },
"",
{ type: "delay", ms: 1000 },
{ text: "You have cascade.exe.", className: "warning" },
{ text: "You have five days.", className: "warning" },
"",
{ text: "What happens next is up to you.", className: "info" },
],
else: [
{ text: "The Lighthouse grows quiet.", className: "info" },
"",
{ type: "delay", ms: 600 },
{ type: "typewriter", text: "SIGNAL LOST", speed: 80 },
"",
{ type: "delay", ms: 800 },
"You disconnect without the file.",
"But the number stays with you.",
"",
{ text: "555-0237", className: "warning" },
"",
{ type: "delay", ms: 600 },
"You could always call back...",
{ text: "If the line is still there.", className: "info" },
],
},
],
next: "carrier_lost",
delay: 2000,
},
};
};

View file

@ -4,6 +4,8 @@
{{ $commandFiles := resources.Match "js/commands/*.js" }} {{ $commandFiles := resources.Match "js/commands/*.js" }}
{{ $subfolderFiles := resources.Match "js/*/*.js" }} {{ $subfolderFiles := resources.Match "js/*/*.js" }}
{{ $deepSubfolderFiles := resources.Match "js/*/*/*.js" }} {{ $deepSubfolderFiles := resources.Match "js/*/*/*.js" }}
{{ $deeperSubfolderFiles := resources.Match "js/*/*/*/*.js" }}
{{ $deepestSubfolderFiles := resources.Match "js/*/*/*/*/*.js" }}
{{ $remaining := resources.Match "js/*.js" }} {{ $remaining := resources.Match "js/*.js" }}
{{ $filtered := slice }} {{ $filtered := slice }}
{{ range $remaining }} {{ range $remaining }}
@ -26,7 +28,26 @@
{{ $filteredDeepSubfolders = $filteredDeepSubfolders | append . }} {{ $filteredDeepSubfolders = $filteredDeepSubfolders | append . }}
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ $allFiles := slice $terminalShell | append $filtered | append $init | append $commandFiles | append $filteredSubfolders | append $filteredDeepSubfolders }} {{ $filteredDeeperSubfolders := slice }}
{{ $gameChapterFiles := slice }}
{{ range $deeperSubfolderFiles }}
{{ $path := .RelPermalink }}
{{ if not (strings.Contains $path "/adoptables/") }}
{{ if strings.Contains $path "/games/games/" }}
{{ $gameChapterFiles = $gameChapterFiles | append . }}
{{ else }}
{{ $filteredDeeperSubfolders = $filteredDeeperSubfolders | append . }}
{{ end }}
{{ end }}
{{ end }}
{{ $filteredDeepestSubfolders := slice }}
{{ range $deepestSubfolderFiles }}
{{ $path := .RelPermalink }}
{{ if not (strings.Contains $path "/adoptables/") }}
{{ $filteredDeepestSubfolders = $filteredDeepestSubfolders | append . }}
{{ end }}
{{ end }}
{{ $allFiles := slice $terminalShell | append $filtered | append $init | append $commandFiles | append $filteredSubfolders | append $filteredDeepSubfolders | append $filteredDeeperSubfolders | append $filteredDeepestSubfolders | append $gameChapterFiles }}
{{ $terminalBundle := $allFiles | resources.Concat "js/terminal-bundle.js" | resources.Minify | resources.Fingerprint }} {{ $terminalBundle := $allFiles | resources.Concat "js/terminal-bundle.js" | resources.Minify | resources.Fingerprint }}
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
<script <script