refactor for shared states and multi chapters
This commit is contained in:
parent
691b809b41
commit
b9bea16f78
13 changed files with 2308 additions and 1786 deletions
|
|
@ -5,12 +5,17 @@ class GameEngine {
|
||||||
this.terminal = null;
|
this.terminal = null;
|
||||||
this.adapter = null;
|
this.adapter = null;
|
||||||
this.state = null;
|
this.state = null;
|
||||||
|
this.sharedState = null; // For series games
|
||||||
this.input = null;
|
this.input = null;
|
||||||
this.sound = null;
|
this.sound = null;
|
||||||
this.scenes = null;
|
this.scenes = null;
|
||||||
|
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
this.originalExecuteCommand = null;
|
this.originalExecuteCommand = null;
|
||||||
|
|
||||||
|
// Series support
|
||||||
|
this.seriesId = gameDefinition.seriesId || null;
|
||||||
|
this.chapterNumber = gameDefinition.chapterNumber || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register this game as a terminal command
|
// Register this game as a terminal command
|
||||||
|
|
@ -35,6 +40,10 @@ class GameEngine {
|
||||||
self._reset();
|
self._reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (args[0] === "reset-series" && self.seriesId) {
|
||||||
|
self._resetSeries();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (args[0] === "continue" || args[0] === "resume") {
|
if (args[0] === "continue" || args[0] === "resume") {
|
||||||
await self.start(true);
|
await self.start(true);
|
||||||
return;
|
return;
|
||||||
|
|
@ -51,6 +60,23 @@ class GameEngine {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize shared state for series games (do this early for chapter check)
|
||||||
|
if (this.seriesId && window.SharedStateManager) {
|
||||||
|
this.sharedState = new SharedStateManager(this.seriesId);
|
||||||
|
this.sharedState.init(this.definition.sharedStateDefaults || {});
|
||||||
|
|
||||||
|
// Check if this chapter can be played (sequential unlock)
|
||||||
|
if (this.chapterNumber && this.chapterNumber > 1) {
|
||||||
|
if (!this.sharedState.canPlayChapter(this.chapterNumber)) {
|
||||||
|
const prevChapter = this.chapterNumber - 1;
|
||||||
|
this.terminal.printError(
|
||||||
|
`You must complete Chapter ${prevChapter} before playing Chapter ${this.chapterNumber}.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update the global HTML to add the game in progress details
|
// Update the global HTML to add the game in progress details
|
||||||
document.body.classList.add("game-in-progress");
|
document.body.classList.add("game-in-progress");
|
||||||
console.log(document.body.classList);
|
console.log(document.body.classList);
|
||||||
|
|
@ -59,7 +85,7 @@ class GameEngine {
|
||||||
|
|
||||||
// Initialize components
|
// Initialize components
|
||||||
this.adapter = new TerminalAdapter(this.terminal);
|
this.adapter = new TerminalAdapter(this.terminal);
|
||||||
this.state = new StateManager(this.definition.id);
|
this.state = new StateManager(this.definition.id, this.sharedState);
|
||||||
this.input = new InputManager(this.adapter);
|
this.input = new InputManager(this.adapter);
|
||||||
|
|
||||||
// Initialize sound manager if SoundManager is available
|
// Initialize sound manager if SoundManager is available
|
||||||
|
|
@ -77,8 +103,11 @@ class GameEngine {
|
||||||
// Initialize state
|
// Initialize state
|
||||||
this.state.init(this.definition.initialState || {});
|
this.state.init(this.definition.initialState || {});
|
||||||
|
|
||||||
|
// Load and merge external scenes if defined
|
||||||
|
const mergedScenes = this._getMergedScenes();
|
||||||
|
|
||||||
// Register scenes
|
// Register scenes
|
||||||
this.scenes.registerScenes(this.definition.scenes);
|
this.scenes.registerScenes(mergedScenes);
|
||||||
|
|
||||||
// Hook into terminal input
|
// Hook into terminal input
|
||||||
this._hookInput();
|
this._hookInput();
|
||||||
|
|
@ -236,6 +265,66 @@ class GameEngine {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset entire series progress (for series games)
|
||||||
|
_resetSeries() {
|
||||||
|
// Reset chapter state
|
||||||
|
this._reset();
|
||||||
|
|
||||||
|
// Reset shared state
|
||||||
|
if (this.seriesId) {
|
||||||
|
const sharedState = new SharedStateManager(this.seriesId);
|
||||||
|
sharedState.reset();
|
||||||
|
this.terminal.printSuccess(
|
||||||
|
`All ${this.definition.name} series progress has been reset.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context for scene factories
|
||||||
|
_createSceneContext() {
|
||||||
|
return {
|
||||||
|
chapterNumber: this.chapterNumber || 1,
|
||||||
|
seriesId: this.seriesId,
|
||||||
|
sharedState: this.sharedState,
|
||||||
|
art: this.definition.art || {},
|
||||||
|
additionalOptions: this.definition.additionalHubOptions || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load external scenes and merge with inline scenes
|
||||||
|
_getMergedScenes() {
|
||||||
|
const inlineScenes = this.definition.scenes || {};
|
||||||
|
|
||||||
|
// If no external scenes defined, just return inline scenes
|
||||||
|
if (
|
||||||
|
!this.definition.externalScenes ||
|
||||||
|
this.definition.externalScenes.length === 0
|
||||||
|
) {
|
||||||
|
return inlineScenes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load external scenes from registered factories
|
||||||
|
const externalScenes = {};
|
||||||
|
const context = this._createSceneContext();
|
||||||
|
|
||||||
|
for (const sceneRef of this.definition.externalScenes) {
|
||||||
|
const factory = window.SceneFactories?.[sceneRef];
|
||||||
|
if (factory) {
|
||||||
|
try {
|
||||||
|
const scenes = factory(context);
|
||||||
|
Object.assign(externalScenes, scenes);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to load external scenes from ${sceneRef}:`, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Scene factory not found: ${sceneRef}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline scenes override external scenes (allows chapter-specific overrides)
|
||||||
|
return { ...externalScenes, ...inlineScenes };
|
||||||
|
}
|
||||||
|
|
||||||
// Debug command to skip to a specific scene
|
// Debug command to skip to a specific scene
|
||||||
async _debugGoToScene(sceneName) {
|
async _debugGoToScene(sceneName) {
|
||||||
const scene = this.scenes.getScene(sceneName);
|
const scene = this.scenes.getScene(sceneName);
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,19 @@ class SceneManager {
|
||||||
this.state.set(action.set, action.value);
|
this.state.set(action.set, action.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set value in shared state (for series games)
|
||||||
|
if (action.setShared !== undefined) {
|
||||||
|
this.state.setShared(action.setShared, action.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark a chapter as complete in shared state
|
||||||
|
if (action.markChapterComplete !== undefined) {
|
||||||
|
const sharedState = this.state.getSharedState();
|
||||||
|
if (sharedState) {
|
||||||
|
sharedState.markChapterComplete(action.markChapterComplete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (action.increment !== undefined) {
|
if (action.increment !== undefined) {
|
||||||
this.state.increment(action.increment, action.amount || 1);
|
this.state.increment(action.increment, action.amount || 1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
assets/js/games/engine/scene-registry.js
Normal file
66
assets/js/games/engine/scene-registry.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Scene Registry - Global registry for shared scene factories
|
||||||
|
// Scene factories are functions that take a context and return scene definitions
|
||||||
|
|
||||||
|
// Initialize global scene factories object
|
||||||
|
window.SceneFactories = window.SceneFactories || {};
|
||||||
|
|
||||||
|
// Helper class for working with scene factories
|
||||||
|
class SceneRegistry {
|
||||||
|
// Register a scene factory
|
||||||
|
static register(id, factory) {
|
||||||
|
if (typeof factory !== "function") {
|
||||||
|
console.error(`Scene factory must be a function: ${id}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
window.SceneFactories[id] = factory;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a scene factory
|
||||||
|
static get(id) {
|
||||||
|
return window.SceneFactories[id] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a factory exists
|
||||||
|
static has(id) {
|
||||||
|
return id in window.SceneFactories;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all registered factories
|
||||||
|
static list() {
|
||||||
|
return Object.keys(window.SceneFactories);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister a factory
|
||||||
|
static unregister(id) {
|
||||||
|
delete window.SceneFactories[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create scenes from a factory with context
|
||||||
|
static createScenes(id, context) {
|
||||||
|
const factory = this.get(id);
|
||||||
|
if (!factory) {
|
||||||
|
console.warn(`Scene factory not found: ${id}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return factory(context);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error creating scenes from factory ${id}:`, e);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge multiple factories into one scene object
|
||||||
|
static mergeFactories(factoryIds, context) {
|
||||||
|
const merged = {};
|
||||||
|
for (const id of factoryIds) {
|
||||||
|
const scenes = this.createScenes(id, context);
|
||||||
|
Object.assign(merged, scenes);
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make available globally
|
||||||
|
window.SceneRegistry = SceneRegistry;
|
||||||
190
assets/js/games/engine/shared-state-manager.js
Normal file
190
assets/js/games/engine/shared-state-manager.js
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
// Shared State Manager - Manages game state across multiple chapters in a series
|
||||||
|
class SharedStateManager {
|
||||||
|
constructor(seriesId) {
|
||||||
|
this.seriesId = seriesId;
|
||||||
|
this.state = {};
|
||||||
|
this.storageKey = `series_${seriesId}_state`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize with default state (only sets values not already in storage)
|
||||||
|
init(defaultState = {}) {
|
||||||
|
this.state = this._deepClone(defaultState);
|
||||||
|
this._loadFromStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get value using dot notation path (e.g., "inventory.sword")
|
||||||
|
get(path, defaultValue = undefined) {
|
||||||
|
const parts = path.split(".");
|
||||||
|
let current = this.state;
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (current === undefined || current === null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
current = current[part];
|
||||||
|
}
|
||||||
|
|
||||||
|
return current !== undefined ? current : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set value using dot notation path
|
||||||
|
set(path, value) {
|
||||||
|
const parts = path.split(".");
|
||||||
|
let current = this.state;
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
if (current[part] === undefined) {
|
||||||
|
current[part] = {};
|
||||||
|
}
|
||||||
|
current = current[part];
|
||||||
|
}
|
||||||
|
|
||||||
|
current[parts[parts.length - 1]] = value;
|
||||||
|
this._saveToStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment a numeric value
|
||||||
|
increment(path, amount = 1) {
|
||||||
|
const current = this.get(path, 0);
|
||||||
|
this.set(path, current + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement a numeric value
|
||||||
|
decrement(path, amount = 1) {
|
||||||
|
this.increment(path, -amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add item to an array (if not already present)
|
||||||
|
addToArray(path, item) {
|
||||||
|
const arr = this.get(path, []);
|
||||||
|
if (!arr.includes(item)) {
|
||||||
|
arr.push(item);
|
||||||
|
this.set(path, arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove item from an array
|
||||||
|
removeFromArray(path, item) {
|
||||||
|
const arr = this.get(path, []);
|
||||||
|
const index = arr.indexOf(item);
|
||||||
|
if (index > -1) {
|
||||||
|
arr.splice(index, 1);
|
||||||
|
this.set(path, arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if array contains item
|
||||||
|
hasItem(path, item) {
|
||||||
|
const arr = this.get(path, []);
|
||||||
|
return arr.includes(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a specific chapter is complete
|
||||||
|
isChapterComplete(chapterNumber) {
|
||||||
|
const completed = this.get("chapters_completed", []);
|
||||||
|
return completed.includes(chapterNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark a chapter as complete
|
||||||
|
markChapterComplete(chapterNumber) {
|
||||||
|
this.addToArray("chapters_completed", chapterNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if chapter can be played (previous chapters complete)
|
||||||
|
canPlayChapter(chapterNumber) {
|
||||||
|
if (chapterNumber <= 1) return true;
|
||||||
|
return this.isChapterComplete(chapterNumber - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the highest completed chapter number
|
||||||
|
getHighestCompletedChapter() {
|
||||||
|
const completed = this.get("chapters_completed", []);
|
||||||
|
return completed.length > 0 ? Math.max(...completed) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entire state (for debugging)
|
||||||
|
getAll() {
|
||||||
|
return this._deepClone(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset all series state
|
||||||
|
reset() {
|
||||||
|
this.state = {};
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(this.storageKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to clear series state from storage:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there is saved state
|
||||||
|
hasSavedState() {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(this.storageKey) !== null;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export state for debugging or backup
|
||||||
|
exportState() {
|
||||||
|
return JSON.stringify(this.state, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import state from backup
|
||||||
|
importState(jsonString) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(jsonString);
|
||||||
|
this.state = data;
|
||||||
|
this._saveToStorage();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to import state:", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveToStorage() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(this.storageKey, JSON.stringify(this.state));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to save series state:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadFromStorage() {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(this.storageKey);
|
||||||
|
if (saved) {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
this.state = this._mergeDeep(this.state, parsed);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to load series state:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_deepClone(obj) {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
_mergeDeep(target, source) {
|
||||||
|
const result = { ...target };
|
||||||
|
for (const key of Object.keys(source)) {
|
||||||
|
if (
|
||||||
|
source[key] &&
|
||||||
|
typeof source[key] === "object" &&
|
||||||
|
!Array.isArray(source[key])
|
||||||
|
) {
|
||||||
|
result[key] = this._mergeDeep(result[key] || {}, source[key]);
|
||||||
|
} else {
|
||||||
|
result[key] = source[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make available globally
|
||||||
|
window.SharedStateManager = SharedStateManager;
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// State Manager - Manages game state with persistence and conditions
|
// State Manager - Manages game state with persistence and conditions
|
||||||
class StateManager {
|
class StateManager {
|
||||||
constructor(gameId) {
|
constructor(gameId, sharedState = null) {
|
||||||
this.gameId = gameId;
|
this.gameId = gameId;
|
||||||
|
this.sharedState = sharedState; // Optional SharedStateManager for series games
|
||||||
this.state = {};
|
this.state = {};
|
||||||
this.storageKey = `game_${gameId}_state`;
|
this.storageKey = `game_${gameId}_state`;
|
||||||
}
|
}
|
||||||
|
|
@ -13,18 +14,38 @@ class StateManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get value using dot notation path (e.g., "inventory.sword")
|
// Get value using dot notation path (e.g., "inventory.sword")
|
||||||
|
// Checks local state first, then falls back to shared state if available
|
||||||
get(path, defaultValue = undefined) {
|
get(path, defaultValue = undefined) {
|
||||||
|
// First check local state
|
||||||
|
const localValue = this._getFromState(this.state, path);
|
||||||
|
if (localValue !== undefined) {
|
||||||
|
return localValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to shared state if available
|
||||||
|
if (this.sharedState) {
|
||||||
|
const sharedValue = this.sharedState.get(path);
|
||||||
|
if (sharedValue !== undefined) {
|
||||||
|
return sharedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper to get value from a specific state object
|
||||||
|
_getFromState(stateObj, path) {
|
||||||
const parts = path.split(".");
|
const parts = path.split(".");
|
||||||
let current = this.state;
|
let current = stateObj;
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (current === undefined || current === null) {
|
if (current === undefined || current === null) {
|
||||||
return defaultValue;
|
return undefined;
|
||||||
}
|
}
|
||||||
current = current[part];
|
current = current[part];
|
||||||
}
|
}
|
||||||
|
|
||||||
return current !== undefined ? current : defaultValue;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set value using dot notation path
|
// Set value using dot notation path
|
||||||
|
|
@ -80,6 +101,25 @@ class StateManager {
|
||||||
return arr.includes(item);
|
return arr.includes(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set value in shared state (for series games)
|
||||||
|
setShared(path, value) {
|
||||||
|
if (!this.sharedState) {
|
||||||
|
console.warn("No shared state manager - setting locally instead");
|
||||||
|
return this.set(path, value);
|
||||||
|
}
|
||||||
|
this.sharedState.set(path, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if path exists in shared state
|
||||||
|
hasShared(path) {
|
||||||
|
return this.sharedState?.get(path) !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get reference to shared state manager
|
||||||
|
getSharedState() {
|
||||||
|
return this.sharedState;
|
||||||
|
}
|
||||||
|
|
||||||
// Evaluate a condition against current state
|
// Evaluate a condition against current state
|
||||||
evaluate(condition) {
|
evaluate(condition) {
|
||||||
if (typeof condition === "boolean") {
|
if (typeof condition === "boolean") {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
561
assets/js/games/games/system-shutdown-1999/chapter-1.js
Normal file
561
assets/js/games/games/system-shutdown-1999/chapter-1.js
Normal file
|
|
@ -0,0 +1,561 @@
|
||||||
|
// System Shutdown: 1999 - Chapter 1: Boxing Day
|
||||||
|
// December 26, 1999 - Five days until the millennium
|
||||||
|
// This is the refactored version using shared scenes and series state
|
||||||
|
|
||||||
|
const CHAPTER1_GLITCH_ART = `
|
||||||
|
▓▓▓▒▒░░ E̸̢R̷̨R̵̢O̸̧R̷̨ ░░▒▒▓▓▓
|
||||||
|
░▒▓█ D̶̨A̷̧T̸̢Ą̵ C̷̢Ǫ̸Ŗ̵R̷̨U̸̢P̵̧T̷̨ █▓▒░
|
||||||
|
▓░▒█ ???????????????? █▒░▓
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CHAPTER1_END_SCREEN = `
|
||||||
|
╔════════════════════════════════════════╗
|
||||||
|
║ ║
|
||||||
|
║ CONNECTION TERMINATED ║
|
||||||
|
║ ║
|
||||||
|
║ Five days remain... ║
|
||||||
|
║ ║
|
||||||
|
╚════════════════════════════════════════╝
|
||||||
|
`;
|
||||||
|
|
||||||
|
const chapter1Game = {
|
||||||
|
// Series integration
|
||||||
|
id: "system-shutdown-1999-chapter-1",
|
||||||
|
seriesId: "system-shutdown-1999",
|
||||||
|
chapterNumber: 1,
|
||||||
|
|
||||||
|
// Game metadata
|
||||||
|
name: "System Shutdown: 1999 - Boxing Day",
|
||||||
|
command: "dial",
|
||||||
|
description: "Connect to Dark Tower BBS - December 26, 1999",
|
||||||
|
|
||||||
|
// Art assets (passed to scene factories via context)
|
||||||
|
art: {
|
||||||
|
CHAPTER1_GLITCH_ART,
|
||||||
|
CHAPTER1_END_SCREEN,
|
||||||
|
},
|
||||||
|
|
||||||
|
// External shared scenes to load
|
||||||
|
externalScenes: [
|
||||||
|
"system-shutdown-1999/dark-tower-hub",
|
||||||
|
"system-shutdown-1999/lighthouse-hub",
|
||||||
|
],
|
||||||
|
|
||||||
|
// Chapter-specific initial state (resets each playthrough)
|
||||||
|
initialState: {
|
||||||
|
// Message discovery
|
||||||
|
read_new_message: false,
|
||||||
|
found_number: false,
|
||||||
|
|
||||||
|
// Scene visit tracking (chapter-local)
|
||||||
|
visited: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Shared state defaults (only set if not already present in series state)
|
||||||
|
sharedStateDefaults: {
|
||||||
|
// Completion tracking
|
||||||
|
chapters_completed: [],
|
||||||
|
|
||||||
|
// Cross-chapter decisions
|
||||||
|
downloaded_cascade: false,
|
||||||
|
talked_to_sysop: false,
|
||||||
|
deleted_corrupted_file: false,
|
||||||
|
route_taken: null,
|
||||||
|
|
||||||
|
// World state changes
|
||||||
|
archives_deleted: false,
|
||||||
|
corrupted_file_deleted: false,
|
||||||
|
|
||||||
|
// Discovery flags (shared so later chapters know)
|
||||||
|
dialed_lighthouse: false,
|
||||||
|
seen_archive_glitch: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
intro: [
|
||||||
|
{
|
||||||
|
type: "ansi",
|
||||||
|
art: BOXING_DAY_TITLE,
|
||||||
|
className: "game-ansi-art center",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{ text: "December 26, 1999 - 10:47 PM", className: "info" },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"Five days until the millennium.",
|
||||||
|
{ type: "delay", ms: 1500 },
|
||||||
|
"Five days until everything might change.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
"Your 56k modem hums quietly in the dark.",
|
||||||
|
"The house is silent. Everyone else is asleep.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 400 },
|
||||||
|
{
|
||||||
|
text: "<strong>This game occasionally plays sounds, mute your tab now if that offends you.</strong>",
|
||||||
|
html: true,
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
{ text: 'Type "quit" at any time to save and exit.', className: "info" },
|
||||||
|
],
|
||||||
|
|
||||||
|
startScene: "connect_prompt",
|
||||||
|
|
||||||
|
// Chapter-specific scenes (these override or extend shared scenes)
|
||||||
|
scenes: {
|
||||||
|
// ==========================================
|
||||||
|
// OPENING SEQUENCE (Chapter 1 specific)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
connect_prompt: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: "<em>Your terminal awaits a command.</em>",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "<em>The familiar glow illuminates your face.</em>",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [{ text: "Connect to Dark Tower BBS", next: "modem_connect" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
modem_connect: {
|
||||||
|
clear: true,
|
||||||
|
sounds: [{ id: "modem_connect", url: "/audio/modem-connect.mp3" }],
|
||||||
|
content: [
|
||||||
|
{ type: "typewriter", text: "ATDT 555-0199", speed: 80 },
|
||||||
|
"",
|
||||||
|
{ type: "sound", id: "modem_connect", volume: 0.6 },
|
||||||
|
{ type: "delay", ms: 400 },
|
||||||
|
{ text: "DIALING...", className: "info" },
|
||||||
|
{ type: "delay", ms: 3000 },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "~~ eEe ~~ EEE ~~ eee ~~",
|
||||||
|
speed: 35,
|
||||||
|
italic: true,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 3000 },
|
||||||
|
"CONNECT 56000",
|
||||||
|
"",
|
||||||
|
{ text: "Carrier detected.", className: "success" },
|
||||||
|
{ type: "delay", ms: 4500 },
|
||||||
|
"Negotiating protocol...",
|
||||||
|
{ type: "delay", ms: 4500 },
|
||||||
|
{ text: "Connection established.", className: "success" },
|
||||||
|
{ type: "delay", ms: 2000 },
|
||||||
|
],
|
||||||
|
next: "dark_tower_main",
|
||||||
|
delay: 1200,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// MESSAGE DISCOVERY (Chapter 1 specific)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
read_messages: {
|
||||||
|
content: [
|
||||||
|
...TableHelper.table({
|
||||||
|
title: "Private Messages for 0BSERVER0",
|
||||||
|
headers: ["#", "FROM", "TO", "DATE", "STATUS"],
|
||||||
|
rows: [
|
||||||
|
[
|
||||||
|
"23",
|
||||||
|
"[UNKNOWN]",
|
||||||
|
"0BSERVER0",
|
||||||
|
"24/12",
|
||||||
|
{ text: "NEW", className: "warning" },
|
||||||
|
],
|
||||||
|
["22", "NIGHTWATCHER", "0BSERVER0", "12/12", "READ"],
|
||||||
|
["21", "0BSERVER0", "NIGHTWATCHER", "11/12", "SENT"],
|
||||||
|
["22", "NIGHTWATCHER", "0BSERVER0", "10/12", "READ"],
|
||||||
|
],
|
||||||
|
widths: [4, 12, 12, 8, 8],
|
||||||
|
align: ["right", "left", "left", "left", "left"],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{ text: "Open unread message", next: "new_message" },
|
||||||
|
{ text: "Back to main menu", next: "dark_tower_main" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
new_message: {
|
||||||
|
content: [
|
||||||
|
{ type: "delay", ms: 300 },
|
||||||
|
"─── BEGIN MESSAGE ───",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "I know you've been searching.",
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "The lighthouse keeper left something behind.",
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "Dial 555-0237 before midnight strikes.",
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
"",
|
||||||
|
{ type: "typewriter", text: "The cascade is coming.", speed: 70 },
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
"",
|
||||||
|
"─── END MESSAGE ───",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
text: "<br /><em>The number burns in your mind: 555-0237</em>",
|
||||||
|
html: true,
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
{
|
||||||
|
text: "<em>Your clock reads 11:54 PM.</em>",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "<em>Six minutes until midnight.</em><br/><br />",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
onEnter: [
|
||||||
|
{ set: "read_new_message", value: true },
|
||||||
|
{ set: "found_number", value: true },
|
||||||
|
{ setShared: "found_number", value: true },
|
||||||
|
],
|
||||||
|
prompt: "What do you do?",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
text: "Dial the number NOW",
|
||||||
|
next: "choice_immediate",
|
||||||
|
actions: [{ setShared: "route_taken", value: "immediate" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Explore Dark Tower first",
|
||||||
|
next: "dark_tower_main",
|
||||||
|
actions: [{ setShared: "route_taken", value: "cautious" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Delete the message and forget it",
|
||||||
|
next: "choice_ignored",
|
||||||
|
actions: [{ setShared: "route_taken", value: "ignored" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
message_archive: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "Private Messages for 0BSERVER0",
|
||||||
|
headers: ["#", "FROM", "TO", "DATE", "STATUS"],
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
condition: { not: "read_new_message" },
|
||||||
|
cells: ["23", "[UNKNOWN]", "0BSERVER0", "24/12", "NEW"],
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "read_new_message",
|
||||||
|
cells: ["23", "[UNKNOWN]", "0BSERVER0", "24/12", "READ"],
|
||||||
|
},
|
||||||
|
["22", "NIGHTWATCHER", "0BSERVER0", "12/12", "READ"],
|
||||||
|
["21", "0BSERVER0", "NIGHTWATCHER", "11/12", "SENT"],
|
||||||
|
["22", "NIGHTWATCHER", "0BSERVER0", "10/12", "READ"],
|
||||||
|
{
|
||||||
|
condition: { and: ["has_secret", { not: "revealed_secret" }] },
|
||||||
|
cells: ["99", "???", "???", "??/??", "HIDDEN"],
|
||||||
|
className: "error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
widths: [4, 12, 12, 8, 8],
|
||||||
|
align: ["right", "left", "left", "left", "left"],
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
{ text: "<em>No new messages.</em>", html: true, className: "info" },
|
||||||
|
{
|
||||||
|
condition: "read_new_message",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: "<em>Just the number... 555-0237...</em>",
|
||||||
|
html: true,
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
options: [{ text: "Back", next: "dark_tower_main" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// CHOICE ROUTES (Chapter 1 specific)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
choice_immediate: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "<em>Your fingers move before doubt can settle.</em>",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{ type: "typewriter", text: "ATH0", speed: 100 },
|
||||||
|
{ type: "delay", ms: 400 },
|
||||||
|
{ text: "NO CARRIER", className: "warning" },
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
text: "<em>You disconnect from Dark Tower.</em>",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "<em>The silence of your room feels heavier now.</em>",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "Something compels you forward...",
|
||||||
|
italic: true,
|
||||||
|
speed: 100,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 1500 },
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "...555-0237",
|
||||||
|
italic: true,
|
||||||
|
speed: 100,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 2000 },
|
||||||
|
],
|
||||||
|
next: "dial_lighthouse",
|
||||||
|
delay: 1000,
|
||||||
|
},
|
||||||
|
|
||||||
|
choice_ignored: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
"You highlight the message.",
|
||||||
|
"",
|
||||||
|
{ type: "typewriter", text: "DELETE MESSAGE? [Y/N]", speed: 50 },
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
{ type: "typewriter", text: "Y", speed: 100 },
|
||||||
|
{ type: "delay", ms: 400 },
|
||||||
|
"",
|
||||||
|
{ text: "MESSAGE DELETED", className: "success" },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
"The number fades from memory.",
|
||||||
|
"Just another piece of BBS spam, you tell yourself.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 2000 },
|
||||||
|
"You browse Dark Tower for another hour.",
|
||||||
|
"Download some wallpapers.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 2000 },
|
||||||
|
"At 11:57 PM, you disconnect.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 2000 },
|
||||||
|
"Five days later, the millennium arrives.",
|
||||||
|
"Fireworks. Champagne. Relief.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"Nothing happens.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 2000 },
|
||||||
|
{ text: "Or does it?", className: "warning" },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 2000 },
|
||||||
|
"You never find out what cascade.exe would have done.",
|
||||||
|
"The lighthouse keeper's message was never meant for you.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
{ text: "Perhaps that's for the best.", className: "info" },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
{ text: "[END - Route C: The Road Not Taken]", className: "warning" },
|
||||||
|
],
|
||||||
|
options: [{ text: "Return to terminal", next: "game_end_ignored" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
game_end_ignored: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{ type: "ascii", art: CHAPTER1_END_SCREEN, className: "game-ascii" },
|
||||||
|
"",
|
||||||
|
{ text: "BOXING DAY", className: "game-title" },
|
||||||
|
{ text: "Day 1 Complete - Route C", className: "info" },
|
||||||
|
"",
|
||||||
|
"You chose not to follow the signal.",
|
||||||
|
"Some doors are better left closed.",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
text: 'Type "dial" to play again, or "dial reset" to start fresh.',
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onEnter: [{ markChapterComplete: 1 }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// CHAPTER 1 SPECIFIC ENDINGS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
sleep_ending: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
"You power down the modem.",
|
||||||
|
"The room falls silent.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"As you drift off to sleep, you think about the message.",
|
||||||
|
"The cascade. The keeper. The lighthouse.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
"Tomorrow, you tell yourself.",
|
||||||
|
"You'll investigate tomorrow.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
"But tomorrow, you'll forget.",
|
||||||
|
"The way everyone forgets.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
{
|
||||||
|
condition: { not: "found_number" },
|
||||||
|
content: {
|
||||||
|
text: "[END - Route C: Peaceful Sleep]",
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
else: { text: "[END - Route B: Postponed]", className: "warning" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [{ text: "Return to terminal", next: "game_end_sleep" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
game_end_sleep: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{ type: "ascii", art: CHAPTER1_END_SCREEN, className: "game-ascii" },
|
||||||
|
"",
|
||||||
|
{ text: "BOXING DAY", className: "game-title" },
|
||||||
|
{ text: "Day 1 Complete", className: "info" },
|
||||||
|
"",
|
||||||
|
"You chose rest over curiosity.",
|
||||||
|
"The lighthouse keeper will have to wait.",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
text: 'Type "dial" to play again, or "dial reset" to start fresh.',
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onEnter: [{ markChapterComplete: 1 }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Final ending scene (overrides shared scene for chapter-specific summary)
|
||||||
|
carrier_lost: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{ type: "delay", ms: 300 },
|
||||||
|
{ type: "typewriter", text: "ATH0", speed: 100 },
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
"",
|
||||||
|
{ text: "NO CARRIER", className: "error" },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1200 },
|
||||||
|
"",
|
||||||
|
{ type: "ascii", art: CHAPTER1_END_SCREEN, className: "game-ascii" },
|
||||||
|
"",
|
||||||
|
{ text: "BOXING DAY", className: "game-title" },
|
||||||
|
{ text: "Day 1 Complete", className: "info" },
|
||||||
|
"",
|
||||||
|
{ text: "─── SESSION SUMMARY ───", className: "info" },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
condition: "downloaded_cascade",
|
||||||
|
content: { text: "[X] Downloaded CASCADE.EXE", className: "success" },
|
||||||
|
else: { text: "[ ] Downloaded CASCADE.EXE", className: "info" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "talked_to_sysop",
|
||||||
|
content: { text: "[X] Spoke with Keeper", className: "success" },
|
||||||
|
else: { text: "[ ] Spoke with Keeper", className: "info" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "deleted_corrupted_file",
|
||||||
|
content: { text: "[X] Accessed SHADOW.DAT", className: "warning" },
|
||||||
|
else: { text: "[ ] Accessed SHADOW.DAT", className: "info" },
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
condition: "archives_deleted",
|
||||||
|
content: {
|
||||||
|
text: "[!] The Archives have been deleted",
|
||||||
|
className: "error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "corrupted_file_deleted",
|
||||||
|
content: {
|
||||||
|
text: "[!] SHADOW.DAT has been removed",
|
||||||
|
className: "error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{ text: "Route: ${route_taken}", className: "info" },
|
||||||
|
"",
|
||||||
|
{ text: "To be continued...", className: "warning" },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
text: 'Type "dial" to replay, or "dial reset" to clear progress.',
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onEnter: [
|
||||||
|
{ set: "day_1_complete", value: true },
|
||||||
|
{ markChapterComplete: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the game when DOM is ready (ensures all scripts including scene factories are loaded)
|
||||||
|
function registerChapter1() {
|
||||||
|
if (window.terminal && window.GameEngine) {
|
||||||
|
const game = new GameEngine(chapter1Game);
|
||||||
|
game.register();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If DOM is already loaded, register immediately, otherwise wait
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", registerChapter1);
|
||||||
|
} else {
|
||||||
|
registerChapter1();
|
||||||
|
}
|
||||||
|
|
@ -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 (!@#$%^&*)
|
|
||||||
33
assets/js/games/scenes/system-shutdown-1999/art/index.js
Normal file
33
assets/js/games/scenes/system-shutdown-1999/art/index.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Art assets index for System Shutdown: 1999 series
|
||||||
|
// These reference the ANSI art defined in ascii-art.js
|
||||||
|
|
||||||
|
// Art constants are loaded from ascii-art.js and made available globally
|
||||||
|
// This file provides a namespace for the series' art assets
|
||||||
|
|
||||||
|
window.SystemShutdown1999Art = window.SystemShutdown1999Art || {};
|
||||||
|
|
||||||
|
// These will be populated by ascii-art.js when it loads
|
||||||
|
// The art constants are:
|
||||||
|
// - BOXING_DAY_TITLE: Chapter 1 title screen
|
||||||
|
// - DARK_TOWER_HEADER: Dark Tower BBS header
|
||||||
|
// - LIGHTHOUSE_HEADER: The Lighthouse BBS header
|
||||||
|
|
||||||
|
// Helper to get art by name
|
||||||
|
window.SystemShutdown1999Art.get = function (name) {
|
||||||
|
switch (name) {
|
||||||
|
case "BOXING_DAY_TITLE":
|
||||||
|
return window.BOXING_DAY_TITLE;
|
||||||
|
case "DARK_TOWER_HEADER":
|
||||||
|
return window.DARK_TOWER_HEADER;
|
||||||
|
case "LIGHTHOUSE_HEADER":
|
||||||
|
return window.LIGHTHOUSE_HEADER;
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown art asset: ${name}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// List available art assets
|
||||||
|
window.SystemShutdown1999Art.list = function () {
|
||||||
|
return ["BOXING_DAY_TITLE", "DARK_TOWER_HEADER", "LIGHTHOUSE_HEADER"];
|
||||||
|
};
|
||||||
89
assets/js/games/scenes/system-shutdown-1999/config.js
Normal file
89
assets/js/games/scenes/system-shutdown-1999/config.js
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Series configuration for System Shutdown: 1999
|
||||||
|
window.SystemShutdown1999Config = {
|
||||||
|
seriesId: "system-shutdown-1999",
|
||||||
|
name: "System Shutdown: 1999",
|
||||||
|
|
||||||
|
// Chapter definitions
|
||||||
|
chapters: [
|
||||||
|
{
|
||||||
|
number: 1,
|
||||||
|
id: "system-shutdown-1999-chapter-1",
|
||||||
|
command: "dial",
|
||||||
|
date: "1999-12-26",
|
||||||
|
title: "Boxing Day",
|
||||||
|
description: "Connect to Dark Tower BBS - December 26, 1999",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 2,
|
||||||
|
id: "system-shutdown-1999-chapter-2",
|
||||||
|
command: "dial2",
|
||||||
|
date: "1999-12-27",
|
||||||
|
title: "Day 2",
|
||||||
|
description: "The day after - December 27, 1999",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 3,
|
||||||
|
id: "system-shutdown-1999-chapter-3",
|
||||||
|
command: "dial3",
|
||||||
|
date: "1999-12-28",
|
||||||
|
title: "Day 3",
|
||||||
|
description: "Three days remain - December 28, 1999",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 4,
|
||||||
|
id: "system-shutdown-1999-chapter-4",
|
||||||
|
command: "dial4",
|
||||||
|
date: "1999-12-29",
|
||||||
|
title: "Day 4",
|
||||||
|
description: "Two days remain - December 29, 1999",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 5,
|
||||||
|
id: "system-shutdown-1999-chapter-5",
|
||||||
|
command: "dial5",
|
||||||
|
date: "1999-12-30",
|
||||||
|
title: "Day 5",
|
||||||
|
description: "The eve - December 30, 1999",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 6,
|
||||||
|
id: "system-shutdown-1999-chapter-6",
|
||||||
|
command: "dial6",
|
||||||
|
date: "1999-12-31",
|
||||||
|
title: "New Year's Eve",
|
||||||
|
description: "The final night - December 31, 1999",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Shared state schema with defaults
|
||||||
|
// These values persist across all chapters
|
||||||
|
sharedStateDefaults: {
|
||||||
|
// Completion tracking
|
||||||
|
chapters_completed: [],
|
||||||
|
|
||||||
|
// Core cross-chapter decisions
|
||||||
|
downloaded_cascade: false,
|
||||||
|
talked_to_sysop: false,
|
||||||
|
deleted_corrupted_file: false,
|
||||||
|
route_taken: null, // "immediate" | "cautious" | "ignored"
|
||||||
|
|
||||||
|
// World state changes (persist across chapters)
|
||||||
|
archives_deleted: false,
|
||||||
|
corrupted_file_deleted: false,
|
||||||
|
|
||||||
|
// Discovery flags
|
||||||
|
found_number: false,
|
||||||
|
dialed_lighthouse: false,
|
||||||
|
seen_archive_glitch: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Helper to get chapter by number
|
||||||
|
getChapter(number) {
|
||||||
|
return this.chapters.find((c) => c.number === number);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Helper to get next chapter
|
||||||
|
getNextChapter(currentNumber) {
|
||||||
|
return this.chapters.find((c) => c.number === currentNumber + 1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,574 @@
|
||||||
|
// Dark Tower BBS Hub - Shared scenes for System Shutdown: 1999
|
||||||
|
// These scenes can be used across multiple chapters
|
||||||
|
|
||||||
|
window.SceneFactories = window.SceneFactories || {};
|
||||||
|
|
||||||
|
window.SceneFactories["system-shutdown-1999/dark-tower-hub"] = function (
|
||||||
|
context,
|
||||||
|
) {
|
||||||
|
const { chapterNumber, art, additionalOptions = [] } = context;
|
||||||
|
|
||||||
|
// Get art from bundle scope (loaded by ascii-art.js in same bundle)
|
||||||
|
// Falls back to context.art if not found (for standalone loading)
|
||||||
|
const DARK_TOWER_HEADER_ART =
|
||||||
|
typeof DARK_TOWER_HEADER !== "undefined"
|
||||||
|
? DARK_TOWER_HEADER
|
||||||
|
: art.DARK_TOWER_HEADER;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// ==========================================
|
||||||
|
// DARK TOWER BBS HUB
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
dark_tower_main: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "ansi",
|
||||||
|
art: DARK_TOWER_HEADER_ART,
|
||||||
|
className: "game-ansi-art center",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
text: "---=[ D A R K T O W E R B B S - E S T. 1 9 9 5 ]=---",
|
||||||
|
className: "info center",
|
||||||
|
},
|
||||||
|
// User count can change based on chapter/state
|
||||||
|
{
|
||||||
|
condition: { path: "chapters_completed", contains: 1 },
|
||||||
|
content: {
|
||||||
|
text: "[ Users Connected - 2 ] - [ SysOp - NightWatchman ]",
|
||||||
|
className: "info center",
|
||||||
|
},
|
||||||
|
else: {
|
||||||
|
text: "[ Users Connected - 3 ] - [ SysOp - NightWatchman ]",
|
||||||
|
className: "info center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ text: "[ Local Time: 10:52 PM ]", className: "info center" },
|
||||||
|
"",
|
||||||
|
// New message notification if not read
|
||||||
|
{
|
||||||
|
condition: { not: "read_new_message" },
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: "*** YOU HAVE 1 NEW PRIVATE MESSAGE ***",
|
||||||
|
className: "warning center",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Show warning if archives were deleted (cascade effect)
|
||||||
|
{
|
||||||
|
condition: "archives_deleted",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: "[!] Some system data appears to be missing...",
|
||||||
|
className: "error center",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onAfterRender: [{ set: "visited.dark_tower_main", value: true }],
|
||||||
|
prompt: "Select:",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
text: "Read Private Messages",
|
||||||
|
next: "read_messages",
|
||||||
|
condition: { not: "read_new_message" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Message Archive",
|
||||||
|
next: "message_archive",
|
||||||
|
condition: "read_new_message",
|
||||||
|
},
|
||||||
|
{ text: "Browse Message Boards", next: "browse_boards" },
|
||||||
|
{ text: "File Library", next: "dark_tower_files" },
|
||||||
|
{ text: "Who's Online", next: "whos_online" },
|
||||||
|
{
|
||||||
|
text: "Dial The Lighthouse (555-0237)",
|
||||||
|
next: "confirm_dial_lighthouse",
|
||||||
|
condition: "found_number",
|
||||||
|
},
|
||||||
|
// Additional options can be passed by chapters
|
||||||
|
...additionalOptions,
|
||||||
|
{ text: "Disconnect", next: "leave_early" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// MESSAGE BOARDS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
browse_boards: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "DARK TOWER / MESSAGE BOARDS",
|
||||||
|
headers: ["#", "NAME", "NEW MSG", "LAST"],
|
||||||
|
rows: [
|
||||||
|
["1", "General Discussion", "8", "24/12"],
|
||||||
|
["2", "Tech Support", "1", "25/12"],
|
||||||
|
["3", "File Updates", "3", "23/12"],
|
||||||
|
// Display the archives or have them deleted
|
||||||
|
{
|
||||||
|
condition: { not: "archives_deleted" },
|
||||||
|
cells: ["4", "ARCHIVED", "-", "-"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "archives_deleted",
|
||||||
|
cells: ["4", "<BOARD REMOVED>", "-", "-"],
|
||||||
|
className: "error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
widths: [4, 20, 10, 8],
|
||||||
|
align: ["right", "left", "left", "left"],
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "archives_deleted",
|
||||||
|
content: {
|
||||||
|
type: "typewriter",
|
||||||
|
italic: true,
|
||||||
|
text: "The archived messages are just... gone...",
|
||||||
|
speed: 80,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prompt: "Select board:",
|
||||||
|
options: [
|
||||||
|
{ text: "General Discussion", next: "board_general" },
|
||||||
|
{ text: "Tech Support", next: "board_tech" },
|
||||||
|
{ text: "File Updates", next: "board_files" },
|
||||||
|
{
|
||||||
|
text: "ARCHIVED",
|
||||||
|
next: "board_archives",
|
||||||
|
condition: { not: "archives_deleted" },
|
||||||
|
},
|
||||||
|
{ text: "Back to main menu", next: "dark_tower_main" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
board_general: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "GENERAL DISCUSSION",
|
||||||
|
headers: ["#", "SUBJECT", "MSG", "LAST"],
|
||||||
|
rows: [
|
||||||
|
["1", "2K Preparation Thread", "243", "25/12"],
|
||||||
|
["2", "Anyone else getting weird messages?", "3", "25/12"],
|
||||||
|
["3", "Happy Boxing Day everyone!", "5", "25/12"],
|
||||||
|
["4", "Best BBS games?", "43", "23/12"],
|
||||||
|
["5", "New user intro thread", "67", "20/12"],
|
||||||
|
],
|
||||||
|
widths: [4, 40, 6, 8],
|
||||||
|
align: ["right", "left", "right", "left"],
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "The usual chatter.",
|
||||||
|
italic: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "found_number",
|
||||||
|
content: {
|
||||||
|
type: "text",
|
||||||
|
italic: true,
|
||||||
|
text: "Nothing about lighthouses...",
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{ text: "Read 'weird messages' thread", next: "thread_weird" },
|
||||||
|
{ text: "Back to boards", next: "browse_boards" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
thread_weird: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "Anyone else getting weird messages?",
|
||||||
|
headers: ["FROM", "TO", "DATE"],
|
||||||
|
rows: [["Static_User", "All", "25/12/99"]],
|
||||||
|
widths: [20, 20, 10],
|
||||||
|
align: ["left", "left", "left"],
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
" Got a strange PM last night. No sender listed.",
|
||||||
|
" Just a phone number and something about a 'cascade'.",
|
||||||
|
" Probably spam, but creepy timing with Y2K coming up.",
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
{ text: "Reply from: NightWatchman [SYSOP]", className: "warning" },
|
||||||
|
" Looking into it. Please forward any suspicious messages.",
|
||||||
|
" And don't dial any numbers you don't recognize.",
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
{ text: "Reply from: [DELETED USER]", className: "error" },
|
||||||
|
" [This post cannot be accessed]",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
{
|
||||||
|
condition: "found_number",
|
||||||
|
content: {
|
||||||
|
text: "<br /><br /><em>You notice your message was similar...</em>",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [{ text: "Back", next: "board_general" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
board_tech: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "TECH SUPPORT",
|
||||||
|
headers: ["#", "SUBJECT", "MSG", "LAST"],
|
||||||
|
rows: [
|
||||||
|
["1", "READ FIRST: Y2K Compliance Guide", "152", "25/12"],
|
||||||
|
["2", "Modem dropping connection at midnight?", "3", "25/12"],
|
||||||
|
["3", "How to increase download speeds", "98", "25/12"],
|
||||||
|
["4", "We are migrating to TELNET/IP on 01/04/00", "429", "11/12"],
|
||||||
|
["5", "Inputs not registering", "2", "29/11"],
|
||||||
|
],
|
||||||
|
widths: [4, 45, 6, 8],
|
||||||
|
align: ["right", "left", "right", "left"],
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Standard tech questions. Nothing unusual.",
|
||||||
|
italic: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [{ text: "Back to boards", next: "browse_boards" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
board_archives: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "THE ARCHIVES",
|
||||||
|
headers: ["#", "SUBJECT", "OP", "LAST"],
|
||||||
|
rows: [
|
||||||
|
["1", "The Lighthouse Project", "NightWatchman", "1998"],
|
||||||
|
["2", "Frequencies and Patterns", "Signal_Lost", "1999"],
|
||||||
|
["3", "RE: Has anyone heard from Keeper?", "[UNKNOWN]", "1999"],
|
||||||
|
],
|
||||||
|
widths: [4, 35, 16, 8],
|
||||||
|
align: ["right", "left", "right", "left"],
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Historical posts, read only...",
|
||||||
|
italic: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: { not: "visited.archive_warning" },
|
||||||
|
content: [
|
||||||
|
"",
|
||||||
|
{ text: "These posts feel... different.", className: "warning" },
|
||||||
|
{ text: "Like echoes from somewhere else.", className: "info" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onEnter: [{ set: "visited.archive_warning", value: true }],
|
||||||
|
options: [
|
||||||
|
{ text: "Read 'The Lighthouse Project'", next: "archive_lighthouse" },
|
||||||
|
{
|
||||||
|
text: "Read 'Frequencies and Patterns'",
|
||||||
|
next: "archive_frequencies",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Read 'Has anyone heard from Keeper?'",
|
||||||
|
next: "archive_keeper",
|
||||||
|
},
|
||||||
|
{ text: "Back to boards", next: "browse_boards" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
archive_lighthouse: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "The Lighthouse Project",
|
||||||
|
headers: ["FROM", "TO", "DATE"],
|
||||||
|
rows: [["NightWatchman [SYSOP]", "All", "15/11/98"]],
|
||||||
|
widths: [25, 15, 10],
|
||||||
|
align: ["left", "left", "left"],
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
"Some of you have asked about the secondary BBS.",
|
||||||
|
"Yes, it exists. No, I can't give you the number.",
|
||||||
|
"",
|
||||||
|
"The Lighthouse was set up by someone who called himself 'Keeper'.",
|
||||||
|
"He said he found something in the noise between stations.",
|
||||||
|
"Patterns that shouldn't exist.",
|
||||||
|
"",
|
||||||
|
"I set up his board as a favor.",
|
||||||
|
"Then one day, he stopped logging in.",
|
||||||
|
"",
|
||||||
|
"The board is still there. Still running.",
|
||||||
|
"I check it sometimes. The files he left behind...",
|
||||||
|
"",
|
||||||
|
"Some doors are better left closed.",
|
||||||
|
{ text: "- NW", className: "info" },
|
||||||
|
],
|
||||||
|
options: [{ text: "Back", next: "board_archives" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
archive_frequencies: {
|
||||||
|
title: "Frequencies and Patterns",
|
||||||
|
content: [
|
||||||
|
"═══ Frequencies and Patterns ═══",
|
||||||
|
{ text: "Posted by: Signal_Lost - March 22, 1999", className: "info" },
|
||||||
|
"",
|
||||||
|
"I've been analyzing radio static for three months.",
|
||||||
|
"There's something there. In the spaces between signals.",
|
||||||
|
"",
|
||||||
|
"It's not random. It's STRUCTURED.",
|
||||||
|
"Like code. Like a message.",
|
||||||
|
"",
|
||||||
|
"Keeper knew. That's why he built cascade.exe.",
|
||||||
|
"To translate. To REVEAL.",
|
||||||
|
"",
|
||||||
|
"I'm close to understanding.",
|
||||||
|
"So close.",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
text: "[User Signal_Lost has not logged in since March 23, 1999]",
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [{ text: "Back", next: "board_archives" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
archive_keeper: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "RE: Has anyone heard from Keeper?",
|
||||||
|
headers: ["FROM", "TO", "DATE"],
|
||||||
|
rows: [["[UNKNOWN]", "All", "20/12/99"]],
|
||||||
|
widths: [25, 15, 10],
|
||||||
|
align: ["left", "left", "left"],
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
"He's still there.",
|
||||||
|
"In The Lighthouse.",
|
||||||
|
"Waiting.",
|
||||||
|
"",
|
||||||
|
"The cascade is ready.",
|
||||||
|
"It just needs carriers.",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
type: "glitch",
|
||||||
|
text: "ERROR: MEMORY FAULT AT 0x555f0237",
|
||||||
|
intensity: 0.7,
|
||||||
|
spread: 0,
|
||||||
|
speed: 200,
|
||||||
|
duration: 2000,
|
||||||
|
className: "error glitch-text",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
"Before midnight on the 31st.",
|
||||||
|
"The alignment only happens once.",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
text: "[This post was flagged for removal but persists]",
|
||||||
|
className: "error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
html: true,
|
||||||
|
text: "<br /><br />",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: { not: "seen_archive_glitch" },
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: "What the hell was that...",
|
||||||
|
italic: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
else: [
|
||||||
|
{
|
||||||
|
text: "The glitch persists...",
|
||||||
|
italic: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "found_number",
|
||||||
|
content: {
|
||||||
|
text: "The memory location looks oddly like the phone number... 555-0237",
|
||||||
|
italic: true,
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onAfterRender: [{ set: "seen_archive_glitch", value: true }],
|
||||||
|
options: [{ text: "Back", next: "board_archives" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
board_files: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "FILE ANNOUNCEMENTS",
|
||||||
|
headers: ["#", "SUBJECT", "MSG", "LAST"],
|
||||||
|
rows: [
|
||||||
|
["1", "1001FONTS.ZIP - Font Collection", "1", "25/12"],
|
||||||
|
["2", "Y2K_FIX.ZIP - Y2K compliance patches", "4", "23/12"],
|
||||||
|
["3", "DOOM_WAD.ZIP - New Doom Levels", "3", "11/12"],
|
||||||
|
["4", "BRUCE.JPEG - Just my dog :-)", "15", "20/11"],
|
||||||
|
["5", "CATS.GIF - All your base are belong to us", "1", "01/11"],
|
||||||
|
],
|
||||||
|
widths: [4, 45, 6, 8],
|
||||||
|
align: ["right", "left", "right", "left"],
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "<em>New fonts... At last...</em>",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "<em>Can't get distracted just yet.</em>",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [{ text: "Back to boards", next: "browse_boards" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
dark_tower_files: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "FILE LIBRARY",
|
||||||
|
headers: ["#", "DIR", "QTY", "UPDATED"],
|
||||||
|
rows: [
|
||||||
|
["1", "/IMAGES", "234", "25/12"],
|
||||||
|
["2", "/GAMES", "67", "12/12"],
|
||||||
|
["3", "/MUSIC", "89", "30/11"],
|
||||||
|
["4", "/UTILS", "156", "23/11"],
|
||||||
|
["5", "/MISC", "13", "09/10"],
|
||||||
|
],
|
||||||
|
widths: [4, 25, 6, 8],
|
||||||
|
align: ["right", "left", "right", "left"],
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "<em>Standard BBS fare. Nothing unusual.</em>",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [{ text: "Back to main menu", next: "dark_tower_main" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
whos_online: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "CONNECTED USERS",
|
||||||
|
headers: ["#", "USER", "LOC", "UPDATED"],
|
||||||
|
rows: [
|
||||||
|
["1", "0BSERVER0", "Main Menu", "10:54 PM"],
|
||||||
|
// Static_User might not be online in later chapters
|
||||||
|
{
|
||||||
|
condition: { not: { path: "chapters_completed", contains: 1 } },
|
||||||
|
cells: ["2", "Static_User", "Message Boards", "10:39 PM"],
|
||||||
|
},
|
||||||
|
["3", "NightWatchman", "SysOp Console", "10:12 PM"],
|
||||||
|
],
|
||||||
|
widths: [4, 15, 15, 8],
|
||||||
|
align: ["right", "left", "right", "left"],
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [{ text: "Back", next: "dark_tower_main" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
leave_early: {
|
||||||
|
content: [
|
||||||
|
"Are you sure you want to disconnect?",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
condition: "found_number",
|
||||||
|
content: {
|
||||||
|
text: "You still have the number: 555-0237",
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
text: "Dial The Lighthouse first",
|
||||||
|
next: "confirm_dial_lighthouse",
|
||||||
|
condition: "found_number",
|
||||||
|
},
|
||||||
|
{ text: "Yes, disconnect", next: "disconnect_early" },
|
||||||
|
{ text: "Stay connected", next: "dark_tower_main" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnect_early: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{ type: "typewriter", text: "ATH0", speed: 100 },
|
||||||
|
{ type: "delay", ms: 400 },
|
||||||
|
{ text: "NO CARRIER", className: "warning" },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"You disconnect from Dark Tower.",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
condition: "found_number",
|
||||||
|
content: [
|
||||||
|
"The number lingers in your mind.",
|
||||||
|
{ text: "555-0237", className: "warning" },
|
||||||
|
"",
|
||||||
|
"You could always dial it directly...",
|
||||||
|
],
|
||||||
|
else: ["Another quiet night online.", "Nothing unusual."],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
text: "Dial 555-0237",
|
||||||
|
next: "dial_lighthouse",
|
||||||
|
condition: "found_number",
|
||||||
|
},
|
||||||
|
{ text: "Go to sleep", next: "sleep_ending" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
confirm_dial_lighthouse: {
|
||||||
|
content: [
|
||||||
|
{ text: "555-0237", className: "warning" },
|
||||||
|
"",
|
||||||
|
"The number from the message.",
|
||||||
|
"Something waits on the other end.",
|
||||||
|
"",
|
||||||
|
{ text: "Your clock reads 11:56 PM.", className: "info" },
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{ text: "Dial The Lighthouse", next: "dial_lighthouse" },
|
||||||
|
{ text: "Not yet", next: "dark_tower_main" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,625 @@
|
||||||
|
// The Lighthouse BBS Hub - Shared scenes for System Shutdown: 1999
|
||||||
|
// These scenes can be used across multiple chapters
|
||||||
|
|
||||||
|
window.SceneFactories = window.SceneFactories || {};
|
||||||
|
|
||||||
|
window.SceneFactories["system-shutdown-1999/lighthouse-hub"] = function (
|
||||||
|
context,
|
||||||
|
) {
|
||||||
|
const { chapterNumber, art, additionalOptions = [] } = context;
|
||||||
|
|
||||||
|
// Get art from bundle scope (loaded by ascii-art.js in same bundle)
|
||||||
|
// Falls back to context.art if not found (for standalone loading)
|
||||||
|
const LIGHTHOUSE_HEADER_ART =
|
||||||
|
typeof LIGHTHOUSE_HEADER !== "undefined"
|
||||||
|
? LIGHTHOUSE_HEADER
|
||||||
|
: art.LIGHTHOUSE_HEADER;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// ==========================================
|
||||||
|
// THE LIGHTHOUSE
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
dial_lighthouse: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{ type: "typewriter", text: "ATDT 555-0237", speed: 80 },
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
"",
|
||||||
|
{ text: "DIALING...", className: "info" },
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
"",
|
||||||
|
{ text: "RING", className: "warning" },
|
||||||
|
{ type: "delay", ms: 1200 },
|
||||||
|
{ text: "RING", className: "warning" },
|
||||||
|
{ type: "delay", ms: 1200 },
|
||||||
|
{ text: "RING", className: "warning" },
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "~~ crackle ~~ hiss ~~ CONNECT 14400",
|
||||||
|
speed: 40,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
"",
|
||||||
|
{ text: "Connection unstable.", className: "warning" },
|
||||||
|
{ text: "Signal degraded.", className: "warning" },
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"",
|
||||||
|
{ type: "typewriter", text: "Welcome to THE LIGHTHOUSE", speed: 40 },
|
||||||
|
{ type: "delay", ms: 300 },
|
||||||
|
{ type: "typewriter", text: "A beacon in the static.", speed: 40 },
|
||||||
|
],
|
||||||
|
onEnter: [{ setShared: "dialed_lighthouse", value: true }],
|
||||||
|
next: "lighthouse_main",
|
||||||
|
delay: 1000,
|
||||||
|
},
|
||||||
|
|
||||||
|
lighthouse_main: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "ascii",
|
||||||
|
art: LIGHTHOUSE_HEADER_ART,
|
||||||
|
className: "game-ascii",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{ text: "T H E L I G H T H O U S E", className: "center" },
|
||||||
|
{ text: "Last updated: 24/12/1999 23:59:59", className: "center" },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
condition: { not: "visited.lighthouse_main" },
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: "Something feels wrong here.",
|
||||||
|
italic: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "The BBS feels... frozen. Abandoned.",
|
||||||
|
italic: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "downloaded_cascade",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: "The signal flickers. Something has changed.",
|
||||||
|
className: "error",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onAfterRender: [{ set: "visited.lighthouse_main", value: true }],
|
||||||
|
prompt: "Navigate:",
|
||||||
|
options: [
|
||||||
|
{ text: "The Keeper's Log", next: "lighthouse_log" },
|
||||||
|
{ text: "Transmissions", next: "lighthouse_transmissions" },
|
||||||
|
{ text: "File Vault", next: "lighthouse_files" },
|
||||||
|
{
|
||||||
|
text: "Request SysOp Chat",
|
||||||
|
next: "chat_request",
|
||||||
|
condition: {
|
||||||
|
and: [{ not: "downloaded_cascade" }, { not: "talked_to_sysop" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Additional options can be passed by chapters
|
||||||
|
...additionalOptions,
|
||||||
|
{ text: "Disconnect", next: "disconnect_choice" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
lighthouse_log: {
|
||||||
|
content: [
|
||||||
|
"═══ THE KEEPER'S LOG ═══",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
text: "Entry 1 - November 3, 1998<br /><br />",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
" I've found something. In the static between radio stations.",
|
||||||
|
" Patterns. Structures. A language, maybe.",
|
||||||
|
{
|
||||||
|
text: "<br />Entry 7 - December 12, 1998<br /><br />",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
" The patterns are getting clearer. They want to be understood.",
|
||||||
|
" They want to SPREAD.",
|
||||||
|
{
|
||||||
|
text: "<br />Entry 15 - March 19, 1999<br /><br />",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
" CASCADE.EXE is complete. A translator. A carrier. A key.",
|
||||||
|
" When run at the right moment, it will open the door.",
|
||||||
|
{
|
||||||
|
text: "<br />Entry 23 - December 24, 1999<br /><br />",
|
||||||
|
html: true,
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
" The alignment approaches.",
|
||||||
|
" Seven days until the millennium.",
|
||||||
|
" I can hear them now. Always.",
|
||||||
|
"",
|
||||||
|
{ type: "typewriter", text: " They are beautiful...", speed: 100 },
|
||||||
|
{ type: "delay", ms: 2000 },
|
||||||
|
],
|
||||||
|
options: [{ text: "Back", next: "lighthouse_main" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
lighthouse_transmissions: {
|
||||||
|
title: "Transmissions",
|
||||||
|
content: [
|
||||||
|
"═══ RECORDED TRANSMISSIONS ═══",
|
||||||
|
"",
|
||||||
|
{ text: "[AUDIO FILES - PLAYBACK UNAVAILABLE]", className: "error" },
|
||||||
|
"",
|
||||||
|
"Transcript excerpts:",
|
||||||
|
"",
|
||||||
|
" TRANS_001.WAV:",
|
||||||
|
' "...signal detected at 1420 MHz..."',
|
||||||
|
"",
|
||||||
|
" TRANS_014.WAV:",
|
||||||
|
' "...pattern repeats every 23 seconds..."',
|
||||||
|
"",
|
||||||
|
" TRANS_047.WAV:",
|
||||||
|
' "...not random... structured... alive?..."',
|
||||||
|
"",
|
||||||
|
" TRANS_099.WAV:",
|
||||||
|
{ text: ' "[TRANSCRIPT CORRUPTED]"', className: "error" },
|
||||||
|
"",
|
||||||
|
{ text: "The audio files existed once.", className: "info" },
|
||||||
|
{ text: "Now only fragments remain.", className: "warning" },
|
||||||
|
],
|
||||||
|
options: [{ text: "Back", next: "lighthouse_main" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
lighthouse_files: {
|
||||||
|
title: "File Vault",
|
||||||
|
content: [
|
||||||
|
"═══ FILE VAULT v2.1 ═══",
|
||||||
|
{ text: '"The keeper\'s collection"', className: "info" },
|
||||||
|
"",
|
||||||
|
{ text: "Available files:", className: "info" },
|
||||||
|
"",
|
||||||
|
" [1] README.TXT 1.2 KB 12/24/1999",
|
||||||
|
" [2] CASCADE.EXE 47.0 KB 12/25/1999",
|
||||||
|
// Corrupted file - conditionally shown
|
||||||
|
{
|
||||||
|
condition: { not: "corrupted_file_deleted" },
|
||||||
|
content: " [3] SHADOW.DAT ??? KB ??/??/????",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: "corrupted_file_deleted",
|
||||||
|
content: { text: " [3] <FILE REMOVED>", className: "error" },
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
condition: { not: "visited.lighthouse_files" },
|
||||||
|
content: {
|
||||||
|
text: "CASCADE.EXE pulses faintly on your screen.",
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onEnter: [{ set: "visited.lighthouse_files", value: true }],
|
||||||
|
prompt: "Select file:",
|
||||||
|
options: [
|
||||||
|
{ text: "View README.TXT", next: "file_readme" },
|
||||||
|
{ text: "Download CASCADE.EXE", next: "download_confirm" },
|
||||||
|
{
|
||||||
|
text: "Access SHADOW.DAT",
|
||||||
|
next: "choice_corrupted",
|
||||||
|
condition: { not: "corrupted_file_deleted" },
|
||||||
|
},
|
||||||
|
{ text: "Back", next: "lighthouse_main" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
file_readme: {
|
||||||
|
title: "README.TXT",
|
||||||
|
content: [
|
||||||
|
"═══ README.TXT ═══",
|
||||||
|
"",
|
||||||
|
"To whoever finds this:",
|
||||||
|
"",
|
||||||
|
"I built cascade.exe to show them.",
|
||||||
|
"To show everyone what I found in the frequencies.",
|
||||||
|
"",
|
||||||
|
"It spreads. It copies. It REVEALS.",
|
||||||
|
"",
|
||||||
|
"Don't be afraid when the old files disappear.",
|
||||||
|
"They were never real anyway.",
|
||||||
|
"",
|
||||||
|
"Run it before midnight on 01/01/2000.",
|
||||||
|
"The alignment only happens once.",
|
||||||
|
"",
|
||||||
|
{ text: " - K", className: "info" },
|
||||||
|
"",
|
||||||
|
{ text: "P.S. Don't try to open SHADOW.DAT.", className: "warning" },
|
||||||
|
{
|
||||||
|
text: " Some doors shouldn't be opened twice.",
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [{ text: "Back to files", next: "lighthouse_files" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
download_confirm: {
|
||||||
|
content: [
|
||||||
|
{ text: "CASCADE.EXE - 47,104 bytes", className: "warning" },
|
||||||
|
"",
|
||||||
|
"The file waits.",
|
||||||
|
"47 kilobytes of unknown code.",
|
||||||
|
"",
|
||||||
|
"The README said it 'reveals' something.",
|
||||||
|
"That old files will 'disappear'.",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
text: "In five days, everyone worries about Y2K bugs.",
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
{ text: "This feels different.", className: "warning" },
|
||||||
|
],
|
||||||
|
prompt: "Download CASCADE.EXE?",
|
||||||
|
options: [
|
||||||
|
{ text: "Yes, download it", next: "choice_download" },
|
||||||
|
{ text: "No, leave it alone", next: "choice_no_download" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
choice_download: {
|
||||||
|
title: "Downloading...",
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{ type: "typewriter", text: "XMODEM TRANSFER INITIATED", speed: 45 },
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"",
|
||||||
|
{ text: "Receiving: CASCADE.EXE", className: "info" },
|
||||||
|
{ type: "delay", ms: 400 },
|
||||||
|
"",
|
||||||
|
{ type: "typewriter", text: "[", speed: 20 },
|
||||||
|
{ type: "typewriter", text: "████████████████████", speed: 80 },
|
||||||
|
{ type: "typewriter", text: "] 100%", speed: 20 },
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
"",
|
||||||
|
{ text: "TRANSFER COMPLETE", className: "success" },
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"",
|
||||||
|
"The file sits in your download folder.",
|
||||||
|
"47,104 bytes of... something.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
{
|
||||||
|
text: "Somewhere, distantly, you hear static.",
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: 'A whisper in the noise: "Thank you."',
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1200 },
|
||||||
|
{ text: "Something has changed.", className: "error" },
|
||||||
|
],
|
||||||
|
onEnter: [
|
||||||
|
{ setShared: "downloaded_cascade", value: true },
|
||||||
|
{ setShared: "archives_deleted", value: true }, // Archives are removed
|
||||||
|
],
|
||||||
|
next: "post_download",
|
||||||
|
delay: 1500,
|
||||||
|
},
|
||||||
|
|
||||||
|
post_download: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{ text: "CONNECTION INTERRUPTED", className: "error" },
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"",
|
||||||
|
"For a moment, your screen fills with symbols.",
|
||||||
|
"Patterns that almost make sense.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
"Then, clarity.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"You remember Dark Tower's board list.",
|
||||||
|
{ text: "Board #3 - The Archives.", className: "info" },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
{ text: "It's gone.", className: "warning" },
|
||||||
|
{ text: "Like it was never there.", className: "warning" },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
{ text: "The cascade has begun.", className: "error" },
|
||||||
|
],
|
||||||
|
next: "closing_message",
|
||||||
|
delay: 2000,
|
||||||
|
},
|
||||||
|
|
||||||
|
choice_no_download: {
|
||||||
|
content: [
|
||||||
|
"Something holds you back.",
|
||||||
|
"47 kilobytes of unknown code.",
|
||||||
|
"From a stranger who hears things in static.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
{ text: "You leave CASCADE.EXE untouched.", className: "info" },
|
||||||
|
"",
|
||||||
|
"The keeper's work remains on the server.",
|
||||||
|
"Waiting for someone else. Or no one.",
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
text: "Chat with the SysOp",
|
||||||
|
next: "chat_request",
|
||||||
|
condition: { not: "talked_to_sysop" },
|
||||||
|
},
|
||||||
|
{ text: "Return to file list", next: "lighthouse_files" },
|
||||||
|
{ text: "Disconnect", next: "disconnect_choice" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
choice_corrupted: {
|
||||||
|
title: "Accessing SHADOW.DAT...",
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{ type: "typewriter", text: "ATTEMPTING TO READ FILE...", speed: 50 },
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
"",
|
||||||
|
{ text: "ERROR: FILE HEADER CORRUPTED", className: "error" },
|
||||||
|
{ type: "delay", ms: 400 },
|
||||||
|
{ text: "ERROR: UNEXPECTED EOF", className: "error" },
|
||||||
|
{ type: "delay", ms: 400 },
|
||||||
|
{ text: "ERROR: ????????????????????", className: "error" },
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
type: "ascii",
|
||||||
|
art: `
|
||||||
|
▓▓▓▒▒░░ E̸̢R̷̨R̵̢O̸̧R̷̨ ░░▒▒▓▓▓
|
||||||
|
░▒▓█ D̶̨A̷̧T̸̢Ą̵ C̷̢Ǫ̸Ŗ̵R̷̨U̸̢P̵̧T̷̨ █▓▒░
|
||||||
|
▓░▒█ ???????????????? █▒░▓
|
||||||
|
`,
|
||||||
|
className: "error",
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
"",
|
||||||
|
"Your screen flickers violently.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
{ text: "A sound from your speakers.", className: "warning" },
|
||||||
|
{
|
||||||
|
text: "A voice, maybe. Or static shaped like words:",
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{ type: "typewriter", text: '"Not yet. Not yet. Not yet."', speed: 90 },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1200 },
|
||||||
|
{
|
||||||
|
text: "SHADOW.DAT has been removed from the file listing.",
|
||||||
|
className: "error",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
{ text: "Some doors shouldn't be opened twice.", className: "warning" },
|
||||||
|
],
|
||||||
|
onEnter: [
|
||||||
|
{ set: "deleted_corrupted_file", value: true },
|
||||||
|
{ setShared: "corrupted_file_deleted", value: true },
|
||||||
|
],
|
||||||
|
options: [{ text: "Return to files", next: "lighthouse_files" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// SYSOP CHAT
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
chat_request: {
|
||||||
|
content: [
|
||||||
|
{ text: "Requesting SysOp chat...", className: "info" },
|
||||||
|
{ type: "delay", ms: 1500 },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "CHAT REQUEST ACCEPTED",
|
||||||
|
speed: 40,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
"",
|
||||||
|
{ text: "═══ SYSOP: Keeper ═══", className: "warning" },
|
||||||
|
],
|
||||||
|
onEnter: [{ setShared: "talked_to_sysop", value: true }],
|
||||||
|
next: "chat_conversation",
|
||||||
|
delay: 600,
|
||||||
|
},
|
||||||
|
|
||||||
|
chat_conversation: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "KEEPER: You found my message.",
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "KEEPER: I wasn't sure anyone would come.",
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 700 },
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "KEEPER: The cascade is ready.",
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "KEEPER: It just needs carriers.",
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 900 },
|
||||||
|
"",
|
||||||
|
// Conditional response based on download
|
||||||
|
{
|
||||||
|
condition: "downloaded_cascade",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "KEEPER: I see you already have it.",
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "KEEPER: Good. Run it when the clock strikes.",
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
else: [
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "KEEPER: Will you carry it?",
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "KEEPER: Will you help it spread?",
|
||||||
|
speed: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1200 },
|
||||||
|
{ type: "typewriter", text: "KEEPER: I can hear them now.", speed: 50 },
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
{ type: "typewriter", text: "KEEPER: In the static.", speed: 60 },
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
{ type: "typewriter", text: "KEEPER: They're beautiful.", speed: 60 },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1500 },
|
||||||
|
{ text: "═══ KEEPER HAS DISCONNECTED ═══", className: "error" },
|
||||||
|
],
|
||||||
|
next: "chat_aftermath",
|
||||||
|
delay: 1500,
|
||||||
|
},
|
||||||
|
|
||||||
|
chat_aftermath: {
|
||||||
|
content: [
|
||||||
|
"The chat window closes.",
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
"Your room feels colder.",
|
||||||
|
"The modem's carrier tone sounds different somehow.",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
text: "Like there's something else on the line.",
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
text: "Download cascade.exe now",
|
||||||
|
next: "download_confirm",
|
||||||
|
condition: { not: "downloaded_cascade" },
|
||||||
|
},
|
||||||
|
{ text: "Disconnect", next: "closing_message" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// CLOSING SEQUENCE
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
disconnect_choice: {
|
||||||
|
content: [
|
||||||
|
"Your hand hovers over the keyboard.",
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
condition: "downloaded_cascade",
|
||||||
|
content: {
|
||||||
|
text: "CASCADE.EXE sits in your downloads, waiting.",
|
||||||
|
className: "warning",
|
||||||
|
},
|
||||||
|
else: {
|
||||||
|
text: "You could still download CASCADE.EXE before you go...",
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
text: "Download cascade.exe first",
|
||||||
|
next: "download_confirm",
|
||||||
|
condition: { not: "downloaded_cascade" },
|
||||||
|
},
|
||||||
|
{ text: "Disconnect now", next: "closing_message" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
closing_message: {
|
||||||
|
clear: true,
|
||||||
|
content: [
|
||||||
|
{ type: "delay", ms: 500 },
|
||||||
|
// Different closing based on download status
|
||||||
|
{
|
||||||
|
condition: "downloaded_cascade",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: "As you prepare to disconnect, a final message appears:",
|
||||||
|
className: "info",
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
{
|
||||||
|
type: "typewriter",
|
||||||
|
text: "SEE YOU ON THE OTHER SIDE",
|
||||||
|
speed: 55,
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
{ type: "typewriter", text: "01/01/2000 00:00:00", speed: 55 },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 1000 },
|
||||||
|
{ text: "You have cascade.exe.", className: "warning" },
|
||||||
|
{ text: "You have five days.", className: "warning" },
|
||||||
|
"",
|
||||||
|
{ text: "What happens next is up to you.", className: "info" },
|
||||||
|
],
|
||||||
|
else: [
|
||||||
|
{ text: "The Lighthouse grows quiet.", className: "info" },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
{ type: "typewriter", text: "SIGNAL LOST", speed: 80 },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 800 },
|
||||||
|
"You disconnect without the file.",
|
||||||
|
"But the number stays with you.",
|
||||||
|
"",
|
||||||
|
{ text: "555-0237", className: "warning" },
|
||||||
|
"",
|
||||||
|
{ type: "delay", ms: 600 },
|
||||||
|
"You could always call back...",
|
||||||
|
{ text: "If the line is still there.", className: "info" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
next: "carrier_lost",
|
||||||
|
delay: 2000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
{{ $commandFiles := resources.Match "js/commands/*.js" }}
|
{{ $commandFiles := resources.Match "js/commands/*.js" }}
|
||||||
{{ $subfolderFiles := resources.Match "js/*/*.js" }}
|
{{ $subfolderFiles := resources.Match "js/*/*.js" }}
|
||||||
{{ $deepSubfolderFiles := resources.Match "js/*/*/*.js" }}
|
{{ $deepSubfolderFiles := resources.Match "js/*/*/*.js" }}
|
||||||
|
{{ $deeperSubfolderFiles := resources.Match "js/*/*/*/*.js" }}
|
||||||
|
{{ $deepestSubfolderFiles := resources.Match "js/*/*/*/*/*.js" }}
|
||||||
{{ $remaining := resources.Match "js/*.js" }}
|
{{ $remaining := resources.Match "js/*.js" }}
|
||||||
{{ $filtered := slice }}
|
{{ $filtered := slice }}
|
||||||
{{ range $remaining }}
|
{{ range $remaining }}
|
||||||
|
|
@ -26,7 +28,26 @@
|
||||||
{{ $filteredDeepSubfolders = $filteredDeepSubfolders | append . }}
|
{{ $filteredDeepSubfolders = $filteredDeepSubfolders | append . }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ $allFiles := slice $terminalShell | append $filtered | append $init | append $commandFiles | append $filteredSubfolders | append $filteredDeepSubfolders }}
|
{{ $filteredDeeperSubfolders := slice }}
|
||||||
|
{{ $gameChapterFiles := slice }}
|
||||||
|
{{ range $deeperSubfolderFiles }}
|
||||||
|
{{ $path := .RelPermalink }}
|
||||||
|
{{ if not (strings.Contains $path "/adoptables/") }}
|
||||||
|
{{ if strings.Contains $path "/games/games/" }}
|
||||||
|
{{ $gameChapterFiles = $gameChapterFiles | append . }}
|
||||||
|
{{ else }}
|
||||||
|
{{ $filteredDeeperSubfolders = $filteredDeeperSubfolders | append . }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ $filteredDeepestSubfolders := slice }}
|
||||||
|
{{ range $deepestSubfolderFiles }}
|
||||||
|
{{ $path := .RelPermalink }}
|
||||||
|
{{ if not (strings.Contains $path "/adoptables/") }}
|
||||||
|
{{ $filteredDeepestSubfolders = $filteredDeepestSubfolders | append . }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ $allFiles := slice $terminalShell | append $filtered | append $init | append $commandFiles | append $filteredSubfolders | append $filteredDeepSubfolders | append $filteredDeeperSubfolders | append $filteredDeepestSubfolders | append $gameChapterFiles }}
|
||||||
{{ $terminalBundle := $allFiles | resources.Concat "js/terminal-bundle.js" | resources.Minify | resources.Fingerprint }}
|
{{ $terminalBundle := $allFiles | resources.Concat "js/terminal-bundle.js" | resources.Minify | resources.Fingerprint }}
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
<script
|
<script
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue