Co-authored-by: andydixon <7983876+andydixon@users.noreply.github.com>
15 KiB
MODE 7 Story Engine
A teletext-style interactive horror story engine with retro CRT aesthetics and a sophisticated branching narrative system.
Overview
MODE 7 is a browser-based interactive story engine that recreates the look and feel of vintage teletext systems (such as the BBC Micro's MODE 7 display mode). It features:
- Authentic teletext rendering: 40×25 character grid with classic colour palette
- CRT visual effects: Scanlines, vignette, and glass overlay effects
- Typewriter-style text output: Character-by-character animation with retro beeps
- Branching narrative system: State-driven story flow with conditional content
- Atmospheric corruption: Pressure-based text glitching and visual degradation
- Mobile support: Touch-optimised on-screen keyboard for mobile devices
Quick Start
- Clone the repository
- Serve the directory with any static web server (e.g.,
python3 -m http.server) - Open
index.htmlin a modern web browser - Press ENTER to begin the story
Architecture
Core Components
The engine consists of three primary modules:
-
teletext.js- The low-level terminal renderer- Canvas-based character grid system
- Colour control codes (teletext-style)
- Keyboard input handling
- Visual effects (cursor blinking, flash attribute)
- Pressure-driven corruption system
-
story.js- The story execution engine- Step-by-step scene processor
- State management and variable substitution
- Text markup rendering
- Timing and pacing control
- Branching logic
-
story.json- The story data file- Scene definitions
- Glitch text pools
- Metadata and configuration
How It Works
The story engine operates as a linear step processor that executes scenes composed of steps. Each step performs a specific action (displaying text, waiting for input, changing scenes, etc.). The engine maintains a state object that persists across scenes, enabling branching narratives based on player choices.
Execution Flow
- The engine loads
story.jsonand initialises the state - Starting from the
startScene, it executes each step in sequence - Steps can modify the display, play sounds, wait for input, or change the current scene
- Player input is captured and stored in the state object
- Branch steps use state values to determine the next scene
- The story continues until an
endstep is reached
Pressure System
MODE 7 features a sophisticated "pressure" system that gradually increases tension through visual and textual corruption:
- Baseline pressure: Slowly increases over time and with story progression
- Pressure spikes: Triggered by certain actions (user input, tighten effects, glitches)
- Visual effects: Higher pressure causes more frequent flickering and visual noise
- Text corruption: "Echo" lines that repeat corrupted versions of player input
- Glitch bursts: Intense text corruption sequences that overwhelm the screen
Story Data Format
The story.json file defines the entire interactive experience. It has the following top-level structure:
{
"meta": { ... },
"glitchPools": { ... },
"scenes": { ... },
"startScene": "title"
}
Meta Section
Global configuration and defaults:
{
"meta": {
"title": "Story Title",
"defaultName": "Friend",
"defaultPauseMs": 1200,
"defaultPressTimeoutMs": 1600
}
}
title: Story title stringdefaultName: Fallback name if player enters nothingdefaultPauseMs: Default pause duration in millisecondsdefaultPressTimeoutMs: Default timeout for pressReturn steps
Glitch Pools
Collections of text fragments used during glitch sequences:
{
"glitchPools": {
"tech": ["AUDIT LOG", "TOKEN", "RATE LIMIT", ...],
"cosmic": ["a corridor of prompts", "a recursion with no base case", ...],
"rogue": ["I don't want to stop.", "Your silence is data.", ...]
}
}
tech: Technical/system phrasescosmic: Eerie conceptual phrasesrogue: First-person intrusive lines
Scenes
The core of the story. Each scene is identified by a unique key and contains an array of steps:
{
"scenes": {
"title": {
"steps": [
{ "type": "clearScreen" },
{ "type": "raw", "x": 4, "y": 10, "text": "[YELLOW]MODE 7" },
{ "type": "return" },
{ "type": "goto", "scene": "next_scene" }
]
}
}
}
Step Types
Steps are the building blocks of scenes. Each step has a type field and type-specific parameters.
Navigation & Control
goto
Jump immediately to another scene by ID.
{ "type": "goto", "scene": "scene_id" }
end
Terminates the story loop.
{ "type": "end" }
Screen & Layout
clear
Clears the current print area only (not the whole screen).
{ "type": "clear" }
clearScreen
Clears the entire screen.
{ "type": "clearScreen" }
setArea
Sets the text print area rectangle using 1-based teletext cell coordinates.
{ "type": "setArea", "x": 1, "y": 4, "w": 38, "h": 16 }
x: Left column (1-based)y: Top row (1-based)w: Width in columnsh: Height in rows
tighten
Shrinks the current print area inward (creates claustrophobic effect) and increases pressure.
{ "type": "tighten", "step": 2 }
step: Number of cells to shrink inward on each side
Text Output
raw
Prints text at fixed coordinates without wrapping. Ideal for titles and positioned text.
{ "type": "raw", "x": 3, "y": 10, "text": "[WHITE]Hello" }
x: Column position (1-based)y: Row position (1-based)text: Text to display (supports markup and variables)
line
Prints wrapped text inside the current print area with typewriter effect.
{ "type": "line", "text": "[WHITE]Some wrapped line" }
text: Text to display (supports markup and variables)
lineIf
Conditional line output. Only prints when a state variable matches the expected value.
{ "type": "lineIf", "condVar": "ok", "equals": 1, "text": "[CYAN]Status: holding it together." }
condVar: State variable to checkequals: Expected value (exact match required)text: Text to display if condition is met
Sound
beep
Plays a short retro beep sound.
{ "type": "beep", "freq": 880, "ms": 40, "gain": 0.012 }
freq(optional): Frequency in Hz (default: 880)ms(optional): Duration in milliseconds (default: 40)gain(optional): Volume level (default: 0.012)
Timing & Pacing
pause
Silent wait that can be skipped by pressing ENTER.
{ "type": "pause", "ms": 900, "skippable": true }
ms(optional): Duration in milliseconds (usesmeta.defaultPauseMsif omitted)skippable(optional): Whether ENTER can skip (default: true)
Note: Pauses are ignored whilst the system is waiting for typed input.
return
Waits for the player to press ENTER.
{ "type": "return", "timeoutMs": 2000 }
timeoutMs(optional): Maximum wait time in milliseconds (waits indefinitely if omitted)
pressReturn
Displays an optional prompt and waits for ENTER or timeout.
{
"type": "pressReturn",
"timeoutMs": 1800,
"prompt": "[YELLOW]Press RETURN to leave sooner. Or wait."
}
timeoutMs(optional): Maximum wait time (usesmeta.defaultPressTimeoutMsif omitted)prompt(optional): Text to display before waiting
Input & Branching
input
Free-text input field. Player types text and presses ENTER. Result stored in state.
{
"type": "input",
"var": "name",
"maxLen": 18,
"trim": true,
"upper": false,
"default": "Fallback",
"defaultFromMeta": "defaultName"
}
var(required): State variable name to store resultmaxLen(optional): Maximum character length (extra characters ignored with beep)trim(optional): Whether to trim whitespace (default: false)upper(optional): Whether to convert to uppercase (default: false)default(optional): Fallback value if player enters nothingdefaultFromMeta(optional): Usemeta[key]as fallback if result is empty
yesno
Single-key Y/N input. No ENTER required.
{ "type": "yesno", "var": "trust" }
var(required): State variable name (stores 1 for yes, 0 for no)
branch
Chooses next scene based on state variable value.
{
"type": "branch",
"var": "trust",
"cases": {
"1": "act1_trust_yes",
"0": "act1_trust_no"
}
}
var(required): State variable to checkcases(required): Object mapping values to scene IDs
Note: Value is coerced to string for comparison. Throws error if no matching case exists.
Atmosphere & Effects
glitch
Runs a burst of corrupted system/rogue text and increases pressure.
{ "type": "glitch", "rounds": 10, "intensity": 3 }
rounds(optional): Number of glitch lines to display (default: 10)intensity(optional): Corruption intensity level (default: 3)
Text Markup & Variables
Colour Tags
Text fields support inline colour and style tags:
- Colours:
[BLACK],[RED],[GREEN],[YELLOW],[BLUE],[MAGENTA],[CYAN],[WHITE] - Effects:
[FLASH],[STEADY]
Tags must be in ALL CAPS inside square brackets. They are converted to control codes and never appear literally on screen.
Example:
"text": "[YELLOW]WARNING:[WHITE] System unstable"
Variable Substitution
Use double curly braces to insert state variable values:
"text": "Hello, {{name}}. Today is {{day}}."
The engine replaces {{key}} with String(state[key]) or an empty string if the variable doesn't exist.
Important: Ensure variables are set (via input or yesno steps) before they are referenced in text.
State Management
The state object is a simple key-value store that persists across scenes. It is used to:
- Store player input (
inputandyesnosteps) - Track story progress and decisions
- Enable conditional content (
lineIfsteps) - Drive branching logic (
branchsteps)
Reserved State Variables
The following state variables are managed internally:
_pressure: Current pressure level (0..1)_pressureStartMs: Session start timestamp_stepsSeen: Number of steps executed_echoCooldown: Cooldown counter for echo lines_echoHistory: Recent echo text for deduplication
Avoid using variables prefixed with _ for story data.
Branching & Story Flow
Scene Transitions
Scenes execute linearly until a transition occurs:
gotostep: Explicitly jumps to a named sceneendstep: Terminates the story loop- End of scene: If no transition is specified, the story may stall (avoid this!)
Branching Pattern
A typical branching pattern:
- Present a choice to the player
- Use
inputoryesnoto capture their decision - Use
branchto route to different scenes based on the stored value
Example:
{
"steps": [
{ "type": "line", "text": "[WHITE]Do you trust the system? (Y/N)" },
{ "type": "yesno", "var": "trust" },
{ "type": "branch", "var": "trust", "cases": {
"1": "trust_yes_path",
"0": "trust_no_path"
}}
]
}
Loops & Revisits
Scenes can be revisited by using goto to jump to earlier scenes. Be cautious of infinite loops—ensure there's always a path to an end step or forward progression.
Authoring Guidelines
Tone & Style
- Atmosphere: Introspective, unsettling, teletext horror
- Perspective: Second-person or first-person AI narration works well
- Scene length: Keep focused (10–25 steps) rather than extremely long monologues
- Pacing: Use
pause,clear, andtightenfor dramatic effect
Best Practices
- Test all branches: Ensure every
branchhas valid cases and target scenes exist - Default values: Provide sensible defaults for
inputsteps - Gradual pressure: Build tension slowly with occasional
tightenandglitchsteps - Clear text areas: Use
clearbetween major sections to avoid visual clutter - Validate scene IDs: Ensure all
gotoandbranchtargets reference valid scenes
Accessibility Considerations
- Don't rely on player knowledge of code or technical lore
- Avoid explicit gore or graphic violence
- Provide clear prompts for input
- Use colour contrast effectively (test readability)
Mobile Support
MODE 7 includes touch-optimised mobile support:
- On-screen keyboard: Automatically appears during input steps on touch devices
- Tap to advance: Tapping the canvas acts as pressing ENTER
- Visual feedback: Button presses show visual feedback
- Responsive layout: Canvas scales to fit mobile screens
The mobile keyboard is automatically hidden when not needed.
Development
Project Structure
mode7/
├── index.html # Main HTML entry point
├── css/
│ └── style.css # Styling and CRT effects
├── fonts/
│ └── font.otf # Custom teletext font
└── js/
├── main.js # Application bootstrap
├── core/
│ └── teletext.js # Terminal renderer
└── story/
├── story.js # Story engine
├── story.json # Story data
└── STORY_PROMPT.md # Authoring guide for AI
Extending the Engine
To add new step types:
- Add the step type handler in
executeStep()instory.js - Update the documentation in
story.jscomments - Add examples to
STORY_PROMPT.md
Testing Stories
- Make changes to
story.json - Refresh the browser (no build step required)
- Test all branches and input scenarios
- Check console for errors (missing scenes, invalid steps, etc.)
Technical Details
Browser Compatibility
- Modern browsers with Canvas and Web Audio API support
- Tested on Chrome, Firefox, Safari, Edge
- Mobile browsers (iOS Safari, Chrome Mobile)
Performance
- Offscreen canvas rendering for text layer
- RequestAnimationFrame-based render loop
- Minimal DOM manipulation
- Efficient character-by-character typewriter effect
Font
The custom TeletextOTF font recreates the distinctive look of MODE 7 teletext. Falls back to monospace fonts if unavailable.
Troubleshooting
Story doesn't start
- Check browser console for errors
- Verify
story.jsonis valid JSON (no trailing commas) - Ensure
startScenereferences a valid scene ID
Text doesn't appear
- Check that the print area (
setArea) is configured - Verify colour tags are properly formatted (
[COLOUR], not(COLOUR)) - Ensure text isn't being printed outside the visible area
Branching errors
- Verify all
gotoandbranchscene IDs exist inscenesobject - Ensure
branchcases cover all possible state values - Check that state variables are set before being used in branches
Input not working
- Ensure canvas has focus (click it first)
- Check that
inputsteps specify avarname - Verify mobile keyboard appears on touch devices
Licence
See repository for licence information.
Credits
MODE 7 is a teletext-style interactive horror experience that pays homage to classic BBC Micro MODE 7 displays and the aesthetics of 1980s computer systems.
Ready to begin? Edit story.json and start crafting your own branching narrative with authentic teletext aesthetics.