diff --git a/assets/js/games/engine/game-engine.js b/assets/js/games/engine/game-engine.js
index 305a3c0..ab6f9e9 100644
--- a/assets/js/games/engine/game-engine.js
+++ b/assets/js/games/engine/game-engine.js
@@ -5,12 +5,17 @@ class GameEngine {
this.terminal = null;
this.adapter = null;
this.state = null;
+ this.sharedState = null; // For series games
this.input = null;
this.sound = null;
this.scenes = null;
this.isRunning = false;
this.originalExecuteCommand = null;
+
+ // Series support
+ this.seriesId = gameDefinition.seriesId || null;
+ this.chapterNumber = gameDefinition.chapterNumber || null;
}
// Register this game as a terminal command
@@ -35,6 +40,10 @@ class GameEngine {
self._reset();
return;
}
+ if (args[0] === "reset-series" && self.seriesId) {
+ self._resetSeries();
+ return;
+ }
if (args[0] === "continue" || args[0] === "resume") {
await self.start(true);
return;
@@ -51,6 +60,23 @@ class GameEngine {
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
document.body.classList.add("game-in-progress");
console.log(document.body.classList);
@@ -59,7 +85,7 @@ class GameEngine {
// Initialize components
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);
// Initialize sound manager if SoundManager is available
@@ -77,8 +103,11 @@ class GameEngine {
// Initialize state
this.state.init(this.definition.initialState || {});
+ // Load and merge external scenes if defined
+ const mergedScenes = this._getMergedScenes();
+
// Register scenes
- this.scenes.registerScenes(this.definition.scenes);
+ this.scenes.registerScenes(mergedScenes);
// Hook into terminal input
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
async _debugGoToScene(sceneName) {
const scene = this.scenes.getScene(sceneName);
diff --git a/assets/js/games/engine/scene-manager.js b/assets/js/games/engine/scene-manager.js
index d67ac85..90ea77e 100644
--- a/assets/js/games/engine/scene-manager.js
+++ b/assets/js/games/engine/scene-manager.js
@@ -322,6 +322,19 @@ class SceneManager {
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) {
this.state.increment(action.increment, action.amount || 1);
}
diff --git a/assets/js/games/engine/scene-registry.js b/assets/js/games/engine/scene-registry.js
new file mode 100644
index 0000000..87d7e22
--- /dev/null
+++ b/assets/js/games/engine/scene-registry.js
@@ -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;
diff --git a/assets/js/games/engine/shared-state-manager.js b/assets/js/games/engine/shared-state-manager.js
new file mode 100644
index 0000000..974dc8e
--- /dev/null
+++ b/assets/js/games/engine/shared-state-manager.js
@@ -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;
diff --git a/assets/js/games/engine/state-manager.js b/assets/js/games/engine/state-manager.js
index 7d25f41..9b9600e 100644
--- a/assets/js/games/engine/state-manager.js
+++ b/assets/js/games/engine/state-manager.js
@@ -1,7 +1,8 @@
// State Manager - Manages game state with persistence and conditions
class StateManager {
- constructor(gameId) {
+ constructor(gameId, sharedState = null) {
this.gameId = gameId;
+ this.sharedState = sharedState; // Optional SharedStateManager for series games
this.state = {};
this.storageKey = `game_${gameId}_state`;
}
@@ -13,18 +14,38 @@ class StateManager {
}
// 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) {
+ // 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(".");
- let current = this.state;
+ let current = stateObj;
for (const part of parts) {
if (current === undefined || current === null) {
- return defaultValue;
+ return undefined;
}
current = current[part];
}
- return current !== undefined ? current : defaultValue;
+ return current;
}
// Set value using dot notation path
@@ -80,6 +101,25 @@ class StateManager {
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(condition) {
if (typeof condition === "boolean") {
diff --git a/assets/js/games/games/boxing-day.js b/assets/js/games/games/boxing-day.js
deleted file mode 100644
index 5f30c18..0000000
--- a/assets/js/games/games/boxing-day.js
+++ /dev/null
@@ -1,1679 +0,0 @@
-// Boxing Day - Day 1: December 26, 1999
-// A BBS-themed mystery game
-
-const GLITCH_ART = `
- ▓▓▓▒▒░░ E̸̢R̷̨R̵̢O̸̧R̷̨ ░░▒▒▓▓▓
- ░▒▓█ D̶̨A̷̧T̸̢Ą̵ C̷̢Ǫ̸Ŗ̵R̷̨U̸̢P̵̧T̷̨ █▓▒░
- ▓░▒█ ???????????????? █▒░▓
-`;
-
-const END_SCREEN = `
- ╔════════════════════════════════════════╗
- ║ ║
- ║ CONNECTION TERMINATED ║
- ║ ║
- ║ Five days remain... ║
- ║ ║
- ╚════════════════════════════════════════╝
-`;
-
-const boxingDayGame = {
- id: "system-shutdown-1999-part-1",
- name: "System Shutdown: 1999 - Part 1",
- command: "dial",
- description: "Connect to Dark Tower BBS - December 26, 1999",
-
- initialState: {
- // Core progression flags
- downloaded_cascade: false,
- talked_to_sysop: false,
- deleted_corrupted_file: false,
- route_taken: null, // "immediate" | "cautious" | "ignored"
-
- // Progression tracking
- read_new_message: false,
- found_number: false,
- dialed_lighthouse: false,
- seen_archive_glitch: false,
-
- // deletion flags (this needs to persist across game sessions somehow... TBD)
- archives_deleted: false,
- corrupted_file_deleted: false,
-
- // Scene visit tracking
- visited: {},
- },
-
- 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: "This game occasionally plays sounds, mute your tab now if that offends you.",
- html: true,
- className: "warning",
- },
- { text: 'Type "quit" at any time to save and exit.', className: "info" },
- ],
-
- startScene: "connect_prompt",
-
- scenes: {
- // ==========================================
- // OPENING SEQUENCE
- // ==========================================
-
- connect_prompt: {
- content: [
- {
- text: "Your terminal awaits a command.",
- html: true,
- className: "info",
- },
- {
- text: "The familiar glow illuminates your face.",
- html: true,
- className: "info",
- },
- ],
- options: [{ text: "Connect to Dark Tower BBS", next: "modem_connect" }],
- },
-
- modem_connect: {
- clear: true,
- // Preload sounds for this scene
- sounds: [{ id: "modem_connect", url: "/audio/modem-connect.mp3" }],
- content: [
- { type: "typewriter", text: "ATDT 555-0199", speed: 80 },
- "",
- // Play modem dial sound
- { 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,
- },
-
- // ==========================================
- // DARK TOWER BBS HUB
- // ==========================================
-
- dark_tower_main: {
- clear: true,
- content: [
- {
- type: "ansi",
- art: DARK_TOWER_HEADER,
- 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",
- },
- {
- 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",
- },
- "",
- ],
- },
- ],
- 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",
- },
- { text: "Disconnect", next: "leave_early" },
- ],
- },
-
- // ==========================================
- // MESSAGE DISCOVERY
- // ==========================================
-
- 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: "
The number burns in your mind: 555-0237",
- html: true,
- className: "warning",
- },
- "",
- { type: "delay", ms: 1000 },
- {
- text: "Your clock reads 11:54 PM.",
- html: true,
- className: "info",
- },
- {
- text: "Six minutes until midnight.
",
- html: true,
- className: "info",
- },
- { type: "delay", ms: 800 },
- "",
- ],
- /**
- * Update variables for read messages and found number
- *
- * The option the user takes here determines the path taken for this chapter
- */
- onEnter: [
- { set: "read_new_message", value: true },
- { set: "found_number", value: true },
- ],
- prompt: "What do you do?",
- options: [
- {
- text: "Dial the number NOW",
- next: "choice_immediate",
- actions: [{ set: "route_taken", value: "immediate" }],
- },
- {
- text: "Explore Dark Tower first",
- next: "dark_tower_main",
- actions: [{ set: "route_taken", value: "cautious" }],
- },
- {
- text: "Delete the message and forget it",
- next: "choice_ignored",
- actions: [{ set: "route_taken", value: "ignored" }],
- },
- ],
- },
-
- message_archive: {
- content: [
- {
- type: "table",
- title: "Private Messages for 0BSERVER0",
- headers: ["#", "FROM", "TO", "DATE", "STATUS"],
- rows: [
- // Conditionally display watcher message as read or not
- {
- 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"],
-
- // Testing the advanced condition stuff...
- {
- 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: "No new messages.", html: true, className: "info" },
- {
- condition: "read_new_message",
- content: [
- {
- text: "Just the number... 555-0237...",
- html: true,
- className: "warning",
- },
- ],
- },
- "",
- ],
- options: [{ text: "Back", next: "dark_tower_main" }],
- },
-
- // ==========================================
- // CHOICE A/B/C - WHEN TO DIAL
- // ==========================================
-
- choice_immediate: {
- clear: true,
- content: [
- {
- type: "text",
- text: "Your fingers move before doubt can settle.",
- html: true,
- className: "info",
- },
- "",
- { type: "typewriter", text: "ATH0", speed: 100 },
- { type: "delay", ms: 400 },
- { text: "NO CARRIER", className: "warning" },
- { type: "delay", ms: 600 },
- "",
- {
- text: "You disconnect from Dark Tower.",
- html: true,
- className: "info",
- },
- {
- text: "The silence of your room feels heavier now.",
- html: true,
- className: "info",
- },
- "",
- "",
- "",
- { type: "delay", ms: 500 },
- //{ text: "Something compels you forward.", className: "info" },
- {
- 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: 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",
- },
- ],
- },
-
- // ==========================================
- // DARK TOWER EXPLORATION
- // ==========================================
-
- 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
- // depending on progress.
- // Not sure if people will be able to go back from lighthouse to tower at this stage
- // Leaving it in just incase I want to do this later...
- {
- condition: { not: "archives_deleted" },
- cells: ["4", "ARCHIVED", "-", "-"],
- },
- {
- condition: "archives_deleted",
- cells: ["4", "", "-", "-"],
- 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: {
- // title: "Thread: Anyone else getting weird messages?",
- 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: "
You notice your message was similar...",
- html: true,
- className: "info",
- },
- },
- ],
- options: [{ text: "Back", next: "board_general" }],
- },
-
- board_tech: {
- // title: "Tech Support",
- 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: {
- // title: "The 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: {
- //title: "The Lighthouse Project",
- 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: {
- //title: "RE: Has anyone heard from 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: "
",
- },
- {
- 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: [
- // Decided to move the phone number out of discovery here..
- // Not sure if it should be found in two places
- // Message should be enough, surely?
- // { set: "found_number", value: true },
- { set: "seen_archive_glitch", value: true },
- ],
- options: [{ text: "Back", next: "board_archives" }],
- },
-
- board_files: {
- // title: "File Announcements",
- 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: "New fonts... At last...",
- html: true,
- className: "info",
- },
- {
- text: "Can't get distracted just yet.",
- html: true,
- className: "info",
- },
- ],
- options: [{ text: "Back to boards", next: "browse_boards" }],
- },
-
- dark_tower_files: {
- //title: "File Library",
- 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: "Standard BBS fare. Nothing unusual.",
- html: true,
- className: "info",
- },
- ],
- options: [{ text: "Back to main menu", next: "dark_tower_main" }],
- },
-
- whos_online: {
- //title: "Who's Online",
- content: [
- {
- type: "table",
- title: "CONNECTED USERS",
- headers: ["#", "USER", "LOC", "UPDATED"],
- rows: [
- ["1", "0BSERVER0", "Main Menu", "10:54 PM"],
- ["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" },
- ],
- },
-
- 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: 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",
- },
- ],
- },
-
- 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" },
- ],
- },
-
- // ==========================================
- // 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: [{ set: "dialed_lighthouse", value: true }],
- next: "lighthouse_main",
- delay: 1000,
- },
-
- lighthouse_main: {
- clear: true,
- content: [
- {
- type: "ascii",
- art: LIGHTHOUSE_HEADER,
- 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" }],
- },
- },
- { text: "Disconnect", next: "disconnect_choice" },
- ],
- },
-
- lighthouse_log: {
- //title: "The Keeper's Log",
- content: [
- "═══ THE KEEPER'S LOG ═══",
- "",
- {
- text: "Entry 1 - November 3, 1998
",
- html: true,
- className: "info",
- },
- " I've found something. In the static between radio stations.",
- " Patterns. Structures. A language, maybe.",
- {
- text: "
Entry 7 - December 12, 1998
",
- html: true,
- className: "info",
- },
- " The patterns are getting clearer. They want to be understood.",
- " They want to SPREAD.",
- {
- text: "
Entry 15 - March 19, 1999
",
- 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: "
Entry 23 - December 24, 1999
",
- 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] ", 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" }],
- },
-
- // ==========================================
- // CHOICE D/E/F - DOWNLOAD DECISION
- // ==========================================
-
- 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: [
- { set: "downloaded_cascade", value: true },
- { set: "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,
- // sounds: [
- // { id: "static", url: "/assets/audio/static.mp3" },
- // { id: "glitch", url: "/assets/audio/glitch.mp3" },
- // ],
- 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 },
- "",
- // Play glitch sound effect
- //{ type: "sound", id: "glitch", volume: 0.5 },
- { type: "ascii", art: GLITCH_ART, className: "error" },
- { type: "delay", ms: 1000 },
- "",
- "Your screen flickers violently.",
- "",
- { type: "delay", ms: 600 },
- { text: "A sound from your speakers.", className: "warning" },
- // Play eerie static with voice
- //{ type: "sound", id: "static", volume: 0.4, duration: 3000, fade: true },
- {
- 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 },
- { set: "corrupted_file_deleted", value: true }, // rm -rf effect
- ],
- 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: [{ set: "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,
- },
-
- 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: 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 }],
- },
- },
-};
-
-// Register the game when terminal is available
-if (window.terminal && window.GameEngine) {
- const game = new GameEngine(boxingDayGame);
- game.register();
-}
diff --git a/assets/js/games/games/system-shutdown-1999/chapter-1.js b/assets/js/games/games/system-shutdown-1999/chapter-1.js
new file mode 100644
index 0000000..ac35277
--- /dev/null
+++ b/assets/js/games/games/system-shutdown-1999/chapter-1.js
@@ -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: "This game occasionally plays sounds, mute your tab now if that offends you.",
+ 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: "Your terminal awaits a command.",
+ html: true,
+ className: "info",
+ },
+ {
+ text: "The familiar glow illuminates your face.",
+ 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: "
The number burns in your mind: 555-0237",
+ html: true,
+ className: "warning",
+ },
+ "",
+ { type: "delay", ms: 1000 },
+ {
+ text: "Your clock reads 11:54 PM.",
+ html: true,
+ className: "info",
+ },
+ {
+ text: "Six minutes until midnight.
",
+ 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: "No new messages.", html: true, className: "info" },
+ {
+ condition: "read_new_message",
+ content: [
+ {
+ text: "Just the number... 555-0237...",
+ html: true,
+ className: "warning",
+ },
+ ],
+ },
+ "",
+ ],
+ options: [{ text: "Back", next: "dark_tower_main" }],
+ },
+
+ // ==========================================
+ // CHOICE ROUTES (Chapter 1 specific)
+ // ==========================================
+
+ choice_immediate: {
+ clear: true,
+ content: [
+ {
+ type: "text",
+ text: "Your fingers move before doubt can settle.",
+ html: true,
+ className: "info",
+ },
+ "",
+ { type: "typewriter", text: "ATH0", speed: 100 },
+ { type: "delay", ms: 400 },
+ { text: "NO CARRIER", className: "warning" },
+ { type: "delay", ms: 600 },
+ "",
+ {
+ text: "You disconnect from Dark Tower.",
+ html: true,
+ className: "info",
+ },
+ {
+ text: "The silence of your room feels heavier now.",
+ 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();
+}
diff --git a/assets/js/games/glitch-example.md b/assets/js/games/glitch-example.md
deleted file mode 100644
index 0baf623..0000000
--- a/assets/js/games/glitch-example.md
+++ /dev/null
@@ -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 (!@#$%^&*)
diff --git a/assets/js/games/scenes/system-shutdown-1999/art/index.js b/assets/js/games/scenes/system-shutdown-1999/art/index.js
new file mode 100644
index 0000000..339e433
--- /dev/null
+++ b/assets/js/games/scenes/system-shutdown-1999/art/index.js
@@ -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"];
+};
diff --git a/assets/js/games/scenes/system-shutdown-1999/config.js b/assets/js/games/scenes/system-shutdown-1999/config.js
new file mode 100644
index 0000000..7d5ad89
--- /dev/null
+++ b/assets/js/games/scenes/system-shutdown-1999/config.js
@@ -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);
+ },
+};
diff --git a/assets/js/games/scenes/system-shutdown-1999/shared/dark-tower-hub.js b/assets/js/games/scenes/system-shutdown-1999/shared/dark-tower-hub.js
new file mode 100644
index 0000000..df10b74
--- /dev/null
+++ b/assets/js/games/scenes/system-shutdown-1999/shared/dark-tower-hub.js
@@ -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", "", "-", "-"],
+ 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: "
You notice your message was similar...",
+ 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: "
",
+ },
+ {
+ 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: "New fonts... At last...",
+ html: true,
+ className: "info",
+ },
+ {
+ text: "Can't get distracted just yet.",
+ 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: "Standard BBS fare. Nothing unusual.",
+ 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" },
+ ],
+ },
+ };
+};
diff --git a/assets/js/games/scenes/system-shutdown-1999/shared/lighthouse-hub.js b/assets/js/games/scenes/system-shutdown-1999/shared/lighthouse-hub.js
new file mode 100644
index 0000000..882a4b4
--- /dev/null
+++ b/assets/js/games/scenes/system-shutdown-1999/shared/lighthouse-hub.js
@@ -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
",
+ html: true,
+ className: "info",
+ },
+ " I've found something. In the static between radio stations.",
+ " Patterns. Structures. A language, maybe.",
+ {
+ text: "
Entry 7 - December 12, 1998
",
+ html: true,
+ className: "info",
+ },
+ " The patterns are getting clearer. They want to be understood.",
+ " They want to SPREAD.",
+ {
+ text: "
Entry 15 - March 19, 1999
",
+ 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: "
Entry 23 - December 24, 1999
",
+ 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] ", 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,
+ },
+ };
+};
diff --git a/layouts/partials/site-scripts.html b/layouts/partials/site-scripts.html
index 4d510e7..c76b17c 100644
--- a/layouts/partials/site-scripts.html
+++ b/layouts/partials/site-scripts.html
@@ -4,6 +4,8 @@
{{ $commandFiles := resources.Match "js/commands/*.js" }}
{{ $subfolderFiles := resources.Match "js/*/*.js" }}
{{ $deepSubfolderFiles := resources.Match "js/*/*/*.js" }}
+{{ $deeperSubfolderFiles := resources.Match "js/*/*/*/*.js" }}
+{{ $deepestSubfolderFiles := resources.Match "js/*/*/*/*/*.js" }}
{{ $remaining := resources.Match "js/*.js" }}
{{ $filtered := slice }}
{{ range $remaining }}
@@ -26,7 +28,26 @@
{{ $filteredDeepSubfolders = $filteredDeepSubfolders | append . }}
{{ 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 }}