From b9bea16f781e22a5670acb2d31041dc15b33596e Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 22 Jan 2026 11:03:05 +0000 Subject: [PATCH] refactor for shared states and multi chapters --- assets/js/games/engine/game-engine.js | 93 +- assets/js/games/engine/scene-manager.js | 13 + assets/js/games/engine/scene-registry.js | 66 + .../js/games/engine/shared-state-manager.js | 190 ++ assets/js/games/engine/state-manager.js | 48 +- assets/js/games/games/boxing-day.js | 1679 ----------------- .../games/system-shutdown-1999/chapter-1.js | 561 ++++++ assets/js/games/glitch-example.md | 100 - .../scenes/system-shutdown-1999/art/index.js | 33 + .../scenes/system-shutdown-1999/config.js | 89 + .../shared/dark-tower-hub.js | 574 ++++++ .../shared/lighthouse-hub.js | 625 ++++++ layouts/partials/site-scripts.html | 23 +- 13 files changed, 2308 insertions(+), 1786 deletions(-) create mode 100644 assets/js/games/engine/scene-registry.js create mode 100644 assets/js/games/engine/shared-state-manager.js delete mode 100644 assets/js/games/games/boxing-day.js create mode 100644 assets/js/games/games/system-shutdown-1999/chapter-1.js delete mode 100644 assets/js/games/glitch-example.md create mode 100644 assets/js/games/scenes/system-shutdown-1999/art/index.js create mode 100644 assets/js/games/scenes/system-shutdown-1999/config.js create mode 100644 assets/js/games/scenes/system-shutdown-1999/shared/dark-tower-hub.js create mode 100644 assets/js/games/scenes/system-shutdown-1999/shared/lighthouse-hub.js 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 }}