A 1990s hacker-themed adventure game with a hybrid terminal/windowed interface reminiscent of early Macintosh computers and the movie "Hackers" (1995). Built with Enyo 1 framework for compatibility with webOS devices and modern browsers.
Target Platforms:
- webOS/LuneOS tablets and phones (legacy WebKit ~Safari 5-6 era)
- Modern browsers (Chrome, Firefox, Safari)
- Android (via Cordova wrapper)
Technical Constraints:
- ES5 JavaScript only (no let/const, arrow functions, etc.)
- Simple CSS (no flexbox, grid, CSS variables)
- Must work offline with optional online features
Status: Complete Date: December 6, 2024
-
Desktop (
source/ui/Desktop.js)- Icon grid with double-tap to launch programs
- CSS-based terminal icon fallback (no image required)
-
Window (
source/ui/Window.js)- Draggable Mac-style windows with touch support
- Title bar with horizontal stripes
- Close button
-
WindowManager (
source/ui/WindowManager.js)- Z-order management (tap to bring to front)
- Window positioning with cascade offset
- Open/close lifecycle
-
Terminal (
source/ui/Terminal.js)- Command input with history (up/down arrows)
- Typewriter effect for output
- Built-in commands: help, clear, echo, whoami, date, about, save, load
-
GameState (
source/core/GameState.js)- Singleton state manager
- Tracks inventory, flags, unlocked programs, command history
- JSON serialization for save/load
-
SaveManager (
source/core/SaveManager.js)- Abstracted save/load system (currently localStorage)
- 10 save slots (0-9)
- Ready for future server-side save implementation
- Methods to replace:
_saveToStorage(),_loadFromStorage()
- Retro CSS (
css/retro.css)- 1-bit Mac aesthetic (black/white window chrome)
- This didn't work well on modern screens, need to think about how to do it better
- Green-on-black terminal display
- Bold 14px monospace text for readability on older displays
- 1-bit Mac aesthetic (black/white window chrome)
-
CSS Gradients don't work on old WebKit
- Solution: Use tiled data URI GIF/PNG images instead
- Use
@supportsto provide gradient override for modern browsers
-
CSS Transforms need -webkit- prefix
- Always include both
-webkit-transformandtransform
- Always include both
-
CSS calc() needs -webkit- prefix
- Use
-webkit-calc()for old Safari
- Use
-
Multiple box-shadows may not work on old WebKit
- Fallback to background images or pseudo-elements
-
Vertical alignment in inputs
- Use
line-heightandvertical-align: middle - May need
@supportsfor browser-specific margin tweaks
- Use
enyo-app/
├── depends.js # Dependency loader
├── index.html # Entry point
├── appinfo.json # App metadata
├── source/
│ ├── App.js # Main application
│ ├── core/
│ │ ├── GameState.js # State management
│ │ └── SaveManager.js # Save/load system
│ └── ui/
│ ├── Desktop.js # Desktop with icons
│ ├── Window.js # Draggable window
│ ├── WindowManager.js # Window lifecycle
│ └── Terminal.js # Terminal emulator
├── css/
│ ├── game.css # Core layout
│ └── retro.css # Retro Mac styling
└── images/
├── icons/ # (placeholder for icons)
└── ui/ # (placeholder for UI assets)
| Command | Description |
|---|---|
| help | Show available commands |
| clear | Clear the terminal |
| echo [text] | Print text to terminal |
| whoami | Display current user |
| date | Show current date and time |
| about | About this system |
| save [0-9] | Save game to slot |
| load [0-9] | Load game from slot |
Status: Complete Date: December 6, 2024
- Renamed game to "Hacker Mystery 95"
- New app icon generated by Gemini Nano Banana image model
- Mac-style persistent menu bar at top of screen
- Hacker Mystery 95 menu: About dialog
- File menu: Save Game, Load Game
- View menu: Show Hidden Files toggle (context-sensitive, only for FileViewer)
- Checkmark indicator for toggled options (using √ for old device compatibility)
- Touch-friendly event handling with old WebKit compatibility
- Title bar stripes only appear on focused window
- Unfocused windows have plain white title bars
- Focus updates on window click, close, and z-order changes
- Reusable modal dialog in App.js
- Used for: About dialog, Save/Load confirmations, Network locked message
- Singleton pattern for global access
- Tree structure with folders and files
- File types: text, folder, encrypted
- Support for locked files (unlock with game flags)
- Support for hidden files/folders
- Encrypted files with password protection and decrypted content
- Chapter 1 story content integrated:
/home/readme.txt- Initial hook/home/documents/- Manifesto, notes pointing to Acid Burn/home/logs/- System and connection logs with BBS hints/home/trash/- Deleted email about Project GIBSON, password fragment/home/secrets/- Hidden folder with encrypted secrets and BBS list
- Finder-style icon grid layout
- Double-tap to open files/folders
- Path bar showing current location
- Visual indicators for locked/hidden files
- Parent folder navigation (..)
- Fires
onOpenFileevent to open files in TextEditor
- View text files from the virtual filesystem
- Info bar with filename
- Scrollable content area
- Password prompt for encrypted files
- Sets game flags when files are decrypted
- HTML-escaped content for security
- Added Files icon (folder-style)
- Added Network icon (locked/disabled)
- CSS-based icons for all desktop items
contentOptionssupport for passing data to window content- Event forwarding from content to App (e.g.,
onOpenFile)
- Player starts with Terminal auto-launched
- Opens Files from Desktop icon
- Explores
/home/readme.txt- mysterious message from "X" - Finds
/home/documents/notes.txt- learns about Acid Burn and BBS - Checks
/home/trash/fragment.txt- discovers password "hackers" - Finds
/home/secrets/folder (hidden, needs discovery) - Opens
too_many_secrets.enc- uses password to decrypt - Learns about Project GIBSON and need to find Acid Burn on BBS
enyo-app/
├── source/
│ ├── App.js # Main app with menu handling, alerts
│ ├── core/
│ │ ├── GameState.js # Singleton state manager
│ │ └── SaveManager.js # localStorage save/load (sync + async APIs)
│ ├── data/
│ │ └── FileSystem.js # Virtual filesystem with hidden files
│ ├── programs/
│ │ ├── FileViewer.js # Icon-grid file browser with Show Hidden
│ │ └── TextEditor.js # Text file viewer with encryption support
│ └── ui/
│ ├── Desktop.js # Desktop with Files, Network, Terminal icons
│ ├── MenuBar.js # NEW: Mac-style menu bar
│ ├── Window.js # Draggable windows with focus indication
│ ├── WindowManager.js # Z-order and focus management
│ └── Terminal.js # Terminal emulator
├── css/
│ └── retro.css # Retro Mac styling including menu bar
└── images/
├── icon.png # App icon (Gemini Nano Banana)
└── dither.gif # Desktop background pattern
- Fixed desktop icons not responding to clicks (manual DOM event binding)
- Fixed FileViewer appending content instead of replacing on navigation
- Fixed window contentOptions not passing through to content components
- Fixed menu dropdowns closing immediately on touch devices
- Fixed z-index stacking for menu dropdowns vs overlay
- Added synchronous
saveGame()/loadGame()convenience methods to SaveManager - Added a beautiful icon for the game, thanks to Nano Banana.
- Named the game "Hacker Mystery 95"
- Enyo's onclick handlers can conflict with manual DOM event bindings
- Solution: Use only manual DOM bindings in
rendered()for complex interactive components - Always include old browser fallbacks:
e = e || window.event; e.preventDefault ? e.preventDefault() : e.returnValue = false; e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true;
- Avoid Unicode characters outside basic Latin (U+0000-U+00FF)
- Use √ (U+221A) instead of ✓ (U+2713) for checkmarks
- Old devices may not render newer Unicode symbols
- When using
createComponent(), theownerproperty determines which component'sthis.$hash contains the new component - Components owned by a parent aren't destroyed when calling
destroyComponents()on a container - Solution: Track dynamically created components in an array and destroy manually
Status: Complete Date: December 7, 2024
- Singleton pattern for global access
- Puzzle states:
locked,available,completed - Register puzzles with:
- Required flags (prerequisites)
- Completion flags (triggers)
- Rewards (flags to set, puzzles to unlock)
- Event system for puzzle completion listeners
- Chapter progress tracking
- Auto-completion chains (completing one puzzle can unlock others)
| Puzzle ID | Name | Trigger |
|---|---|---|
decrypt_secrets |
Too Many Secrets | Decrypt the encrypted file |
find_acid_burn |
Finding Acid Burn | Contact Acid Burn on BBS |
chapter1_complete |
Chapter 1 Complete | Gain Acid Burn's trust |
Instead of a separate Email Client, email is integrated into the BBS systems accessed via the terminal - true to the 1995 experience.
BBSData (source/data/BBSData.js)
- Data definitions for all BBS systems
- Message boards with conditional visibility
- Email system with flag-based message unlocking
- Reply system with trigger words
BBSHandler (source/core/BBSHandler.js)
- Session state machine (password, main menu, boards, email, etc.)
- Menu navigation (boards, email, who's online, help)
- Message board browsing
- Email inbox with read/unread tracking
- Reply composition with trigger-word detection
- Automatic flag setting on story progression
| Phone | Name | Status | Notes |
|---|---|---|---|
| 555-0199 | The Underground | Active | Password protected, main story BBS |
| 555-0134 | CyberDen | Banned | Player is banned |
| 555-2176 | PhreakHole | Dead | Shut down by feds |
| 555-0200 | The Gibson Files | Locked | Future content |
- New commands:
dial <number>,hangup - BBS session routing (input goes to BBSHandler when connected)
- Password prompt mode
- "Press any key" handling for menus
- Notifies PuzzleEngine when files are decrypted
- Consistent flag naming (strips file extension)
- Player explores filesystem, finds hints about BBS
- Discovers password "hackers" in trash folder
- Decrypts
too_many_secrets.enc- learns about GIBSON → Puzzle 1: Too Many Secrets - Uses terminal:
dial 555-0199to connect to The Underground - Enters password "hackers"
- Reads Acid Burn's message on General board
- Replies to Acid Burn on board → Puzzle 2: First Contact
- Returns to main menu, gets new email notification
- Reads email from Acid Burn asking about what player found
- Replies mentioning "gibson" → Puzzle 3: Prove Yourself
- Returns to main menu, checks email again
- Reads Acid Burn's response about trusting player → Puzzle 4: Trusted Hacker
- Goes to Boards, Elite Section now visible
- Reads Acid Burn's message about inside contact → Puzzle 5: Inside Information
- Reads unlocked
contacts.txtfile → Setsknows_gibson_numberflag for Chapter 2
enyo-app/
├── source/
│ ├── App.js
│ ├── core/
│ │ ├── GameState.js
│ │ ├── SaveManager.js
│ │ ├── PuzzleEngine.js # NEW: Puzzle management
│ │ └── BBSHandler.js # NEW: BBS session handler
│ ├── data/
│ │ ├── FileSystem.js
│ │ └── BBSData.js # NEW: BBS content data
│ ├── programs/
│ │ ├── FileViewer.js
│ │ └── TextEditor.js # Updated: PuzzleEngine integration
│ └── ui/
│ ├── Desktop.js
│ ├── MenuBar.js
│ ├── Window.js
│ ├── WindowManager.js
│ └── Terminal.js # Updated: BBS commands
| Command | Description |
|---|---|
| dial [number] | Dial a BBS (e.g., dial 555-0199) |
| hangup | Disconnect from current BBS |
- Displays puzzle completion count in top-right of menu bar
- Clicking shows dropdown: "Hacked X of Y puzzles"
- Updates in real-time when puzzles are completed
- Cross-compatible audio system
- Web Audio API for modern browsers (with buffer preloading)
- HTML5 Audio fallback for older devices (webOS)
- Graceful failure - sound errors never break game logic
onEndedcallback support for sequenced audio- Preloads sounds at startup
| Sound | Trigger |
|---|---|
success.mp3 |
Puzzle completed (score increases) |
dialup.mp3 |
Dialing a valid BBS |
dialup-fail.mp3 |
Dialing an unknown number |
dialup-noservice.mp3 |
Dialing a disconnected BBS (PhreakHole) |
victory.mp3 |
Completing Chapter 1 (connecting to Gibson Files) |
- Phone format validation:
dialcommand requires XXX-XXXX format with helpful error message - Password flexibility: Both "hacker" and "hackers" accepted (case-insensitive)
- GameState → PuzzleEngine integration:
setFlag()automatically notifies puzzle engine - Delayed BBS replies: Board posts and emails queued until returning to main menu
- New message indicators: "NEW MAIL" for email, "(New Reply!)" for board messages in main menu
- Per-BBS dial sounds: Each BBS can specify its own dial sound (e.g.,
dialup-noservice.mp3for dead lines) - Connect sounds: BBS can specify a sound to play on successful connection (e.g., victory sound)
- Input disabled during dialing: Prevents command input race conditions while dial sound plays
- Puzzle timing separation: "Prove Yourself" triggers when sending reply, "Trusted Hacker" triggers when reading response email
- Chapter 1 ending: Gibson Files BBS shows congratulations message with victory sound
| # | Puzzle ID | Name | Trigger | Required Flags |
|---|---|---|---|---|
| 1 | decrypt_secrets |
Too Many Secrets | Decrypt too_many_secrets.enc |
None |
| 2 | reply_to_acid_burn |
First Contact | Reply to Acid Burn on BBS board | None |
| 3 | prove_yourself |
Prove Yourself | Send email to Acid Burn mentioning GIBSON | replied_to_acid_burn + decrypted_too_many_secrets |
| 4 | gain_trust |
Trusted Hacker | Read Acid Burn's response email | contacted_acid_burn |
| 5 | inside_contact |
Inside Information | Read Elite board message from Acid Burn | acid_burn_trusts_player |
enyo-app/
├── source/
│ ├── App.js
│ ├── core/
│ │ ├── GameState.js # Updated: notifies PuzzleEngine on flag changes
│ │ ├── SaveManager.js
│ │ ├── PuzzleEngine.js
│ │ ├── BBSHandler.js # Updated: delayed message delivery
│ │ └── SoundManager.js # NEW: Cross-compatible audio
│ ├── data/
│ │ ├── FileSystem.js
│ │ └── BBSData.js
│ ├── programs/
│ │ ├── FileViewer.js
│ │ └── TextEditor.js # Updated: accepts hacker/hackers password
│ └── ui/
│ ├── Desktop.js
│ ├── MenuBar.js # Updated: score counter
│ ├── Window.js
│ ├── WindowManager.js
│ └── Terminal.js # Updated: dialup sound, format validation
├── sounds/
│ ├── success.mp3 # Puzzle completion sound
│ ├── dialup.mp3 # BBS dialing sound (~11 seconds)
│ ├── dialup-fail.mp3 # Invalid number dial sound
│ ├── dialup-noservice.mp3 # Disconnected number sound
│ └── victory.mp3 # Chapter completion fanfare
cloneNode()on Audio elements causesINVALID_STATE_ERRon old WebKit- Solution: Create fresh
Audio()element each time, wait forcanplayevent - Always wrap audio operations in try-catch to prevent game logic failures
- Call
onEndedcallback even when audio fails so game continues
- Update game state (score) BEFORE playing sounds
- Wrap sound playback in try-catch
- Provide
onEndedcallback that fires even on failure - Check
audioSupportedflag before any audio operations
Status: Complete Date: December 24, 2024
- Issue: After posting a reply to a board message, pressing [B] went to the boards list instead of the message list
- Fix: Changed state from
STATE_READING_BOARDtoSTATE_READING_MESSAGEafter posting, so [B] correctly maps toshowBoardMessages()
- Issue: After viewing Help or Who's Online, pressing any key did nothing - user was stuck
- Root Cause:
showHelp()andshowWhosOnline()returnedwaitForKey: truebut didn't change the session state, so input was still processed byhandleMainMenu()which expected a valid menu choice - Fix: Added new state
STATE_PRESS_ANY_KEYthat returns to main menu on any input
- Issue: In Terminal, empty input was rejected before reaching the
waitForKeyhandler - Fix: Modified
executeCurrentInput()to allow empty input whenbbsWaitingForKeyis true, and skip echo/history for "press any key" mode
- Issue: Closing the terminal window while connected to a BBS left the connection in a stale state
- Fix: Added
bbsHandler.disconnect()call in Terminal'sdestroy()method
Added an online guestbook to The Underground BBS (555-0199).
New States:
STATE_GUESTBOOK- Viewing guestbook entriesSTATE_SIGNING_GUESTBOOK_NAME- Entering name to signSTATE_SIGNING_GUESTBOOK- Entering message to sign
Features:
[S] See Guestbookoption in main menu- Fetches guestbook.csv from remote server with cache-busting
- Displays entries in BBS style with formatted dates (using
toLocaleDateString()) - Two-step signing process: name first, then message
- Profanity filter using bad words list from GitHub
- Offline fallback to hardcoded entry when server unreachable
- Protocol detection - uses http by default, upgrades to https if app is served over https (for older devices with TLS issues)
New Methods:
showGuestbook()- Fetches and displays guestbookfetchGuestbook(url, callback)- Async fetch helperdisplayGuestbook(data)- Renders guestbook entrieshandleGuestbook(input)- Handles S/M menu choicehandleSigningGuestbookName(input)- Collects usernamehandleSigningGuestbook(input)- Collects message and POSTs to serverloadBadWords()- Fetches profanity list on startupcontainsProfanity(text)- Checks text with word boundary matchingupgradeUrlsIfSecure()- Upgrades http to https if neededparseCSVLine(line)- Parses CSV handling quoted fields
Features:
- Receives POST with username and message
- Profanity filter with word boundary matching (same as client)
- Caches bad words list in
data/badwords_cache.txtfor 30 days - Appends entries to
data/guestbook.csv - Keeps only last 100 entries
- CORS headers for cross-origin access
- Error suppression to prevent broken responses
Security:
- Sanitizes username (alphanumeric only, max 20 chars)
- Truncates message to 200 chars
- Escapes CSV special characters (commas, quotes, newlines)
- Simple substring matching causes false positives (e.g., "hello" contains "hell")
- Solution: Use regex with word boundaries
\b - JavaScript:
new RegExp("\\b" + word + "\\b", "i") - PHP:
'/\b' . preg_quote($word, '/') . '\b/i'
- Strip commas from user input to prevent CSV injection
- Alternatively, wrap fields containing commas/quotes in double quotes and escape internal quotes
- PHP warnings output to response body can break JSON/text responses
- Use
error_reporting(0)at script start - Use
@prefix on file operations that might fail - Store cache files in writable
data/directory, not code directory
- Older devices may have issues with modern TLS certificates
- Default to http for maximum compatibility
- Upgrade to https only if app is already served over https:
window.location.protocol === "https:"
enyo-app/
├── source/
│ ├── core/
│ │ └── BBSHandler.js # Updated: guestbook, bug fixes
│ └── ui/
│ └── Terminal.js # Updated: press-any-key fix, disconnect on close
├── server/
│ ├── guestbook.php # NEW: Guestbook API
│ └── data/
│ ├── guestbook.csv # Guestbook entries
│ └── badwords_cache.txt # Cached profanity list
Status: Planning Date: TBD
- Player learned about Project GIBSON - a supercomputer at Ellingson Mineral with a backdoor
- Acid Burn is now an ally and has unlocked the Elite section
- Crash Override is identified as an inside contact at Ellingson (from contacts.txt)
- Player knows Crash Override's modem number: 555-0200 (The Gibson Files BBS)
- The goal: Find proof of the backdoor/worm before "they" use it
- Activate The Gibson Files BBS with actual content
- Communication with Crash Override
- Infiltrating Ellingson Mineral systems
- Finding evidence of the worm/backdoor
- New puzzles and file discoveries
- Possible new programs (e.g., hex editor, network scanner)
- Expand BBSData with Gibson Files content
- Add new files to FileSystem for Chapter 2 discoveries
- Consider adding a "Network" program to access remote systems
- New puzzle chain for Chapter 2
- Use nginx with proper permissions for local development
- Ensure
_wwwuser has execute permission on all parent directories - nginx config requires trailing slashes when using
aliasdirective
# webOS/LuneOS
./build.sh webos
# Web
./build.sh www
# Android
./build.sh android