# Moonlit Engine — design spec A SCUMM-style, web-based adventure game **engine**. This repo is the engine only: game ideas, art and music generation come later. The engine is content-agnostic and data-driven — a game is a JSON spec, not code. Status: v0.1 — core complete and tested (68 tests), web harness working, asset *integration* in place, asset *generation* explicitly out of scope. --- ## 1. Design principles 1. **Headless core.** `src/core/` has zero DOM, zero timers, zero network. It takes an injected clock and emits events. Everything runs under `node --test`. The browser layer (`web/`) is ~150 lines of glue. 2. **Declarative everything.** Conditions, effects, dialogue, scenes and interactions are plain JSON. No `eval`, ever. Specs and saves stay inspectable, validatable and diffable. 3. **Report every problem at once.** The loader validates the whole spec in one pass and lists *all* errors (agents iterate on full error lists, not one crash at a time). Same house rule as the midi-playlist repo. 4. **Never declare success without a passing run.** `./tools/run_all.sh` (validate + tests) must be green before any claim of "works". 5. **Degrade gracefully while art doesn't exist yet.** Missing asset files draw labelled placeholder boxes and warn once per id. A game with zero art files is fully playable for design purposes. ## 2. Architecture ``` game.json ──► loader.js (validate + compile) ┌─► canvas.js (draws) │ ├─► hud.js (verb bar / text box / squares) ▼ ├─► audio.js (music w/ loop points, sfx) engine.js ◄── tick(dt) from rAF or fake clock └─► boot.js (DOM glue, save slots) │ ├── world.js mutable state: room, inventory, flags, vars, characters ├── verbs.js verb-coin interaction resolution ├── dialogue.js branching conversation trees ├── walkable.js polygons, clamping, Dijkstra paths ├── conditions.js JSON condition evaluator (no eval) ├── effects.js JSON world mutations (no eval) ├── save.js versioned JSON snapshots └── events.js pub/sub; renderers/audio subscribe, core never calls them ``` The core emits events (`roomEnter`, `say`, `dialogueLine`, `playMusic`, `walkStart`, …). Renderers subscribe. The core never knows a browser exists. ## 3. Lifecycle Phases: `boot → title → menu → playing → ended`. - **Title screen** — web layer's business (a DOM overlay in the demo). The engine exposes `newGame()`, `load()`, `startScene()`. - **Intro** — just a *scene* (`scenes.intro` in the spec): timed slides, text, music, then `goto` the first room. Skippable (`skipScene()`). - **Menus** (save/load/options/volume) — web layer; the engine exposes `hud.canSave()` and save/load APIs. Keeping menus out of the core means a future game can dress them however it likes. - **Playing** — the verb/click/dialogue loop, with scene and dialogue overlays. - **Ended** — an `endGame` effect sets `phase = "ended"`; clicks stop; the web layer shows the ending screen. ## 4. The game spec (game.json) Top-level keys: `meta`, `startRoom`, `startInventory`, `walkSpeed`, `defaultVerb`, `moveVerb`, `verbs[]`, `rooms[]`, `objects[]`, `characters[]`, `interactions[]`, `dialogues{}`, `scenes{}`, `music[]`, `sfx[]`, `assets[]`. ### Rooms & walkable areas A room is a background asset id, one or more **walkable polygons** (`[[x,y],...]` in a 640×480 coordinate space), an `entry` point, `exits` (`{to, at:[x,y], label}`) and optional `onEnter` effects. The loader warns (never errors) when an exit has no return exit — adventure convention, not a hard rule. ### Objects `{id, room, name, hotspot:{x,y,w,h}, takes?, tags?, sprite?}`. `takes: true` objects leave the room once in inventory. `room: "*"` = everywhere. `tags` feed tag-based interactions. ### Characters Must include one with `id: "player"` (the walker). Others have a room, position, sprite and tags. Characters render back-to-front by y. ### Verbs & interactions (the verb coin) The verb table is free-form: classic nine-coin, modern two-verb, whatever. Each verb has a label and a **default response** — the engine always answers. Interactions are a flat list: `{verb, noun | tag, with?, conditions?, effects?, response?, priority?}`. Resolution order: **exact noun id > tag > `*`**, then higher `priority`, then spec order. Conditions gate each candidate; first full match wins. `with` marks item-with interactions (`give mug to barkeep`). ### Conditions (JSON, no eval) `{flag}`, `{not}`, `{all:[...]}`, `{any:[...]}`, `{var, op: eq|ne|lt|lte|gt|gte, value}`, `{has: item}`, `{at: room}`, `{here: character}`, `{dialogue, node}`, `{seen: scene}`. ### Effects (JSON, no eval) `setFlag`, `setVar`, `addVar`, `give`, `remove`, `goTo`, `moveTo`, `walkTo`, `say`, `playMusic`, `stopMusic`, `playSfx`, `startDialogue`, `startScene`, `animate`, `endGame`. ### Dialogue Named dialogues with a `start` node and a node map. Nodes: `speaker`, `text`, then either `choices[]` (waits for the player; each choice has `to`/`end`, optional `conditions`/`effects`) or `next` (auto-advances on a timed hold) or neither (ends). Nodes can fire `onEnter` effects. Mid-conversation position lives in world state, so saves resume conversations. Finished dialogues are marked done and never auto-resume. ### Scenes (intro / cutscenes) Ordered steps: `music`, `stopMusic`, `sfx`, `slide {asset, ms}`, `text {text, ms}`, `wait {ms}`, `setFlag`, `startDialogue`, `goto {room}`. Instant steps fall through; timed ones wait on the clock. Skipping jumps to the final `goto`. ## 5. Game mechanics - **Movement**: click a walkable point → pathfind (Dijkstra over boundary waypoints between polygons, endpoints line-of-sight joined) → the engine moves the player at `walkSpeed` px/s on `tick(dt)`. Clicks outside the walkable area clamp to the nearest edge. Doors are exits: walk verb + exit click changes rooms and fires `onEnter`. - **Inventory**: `give`/`remove` effects; `has` conditions; taken objects disappear from rooms; the HUD lists names. - **World state**: flags (booleans) and vars (numbers) — the only way stories remember things. Unknown flags read false / vars read 0, so specs may gate on flags created later. - **Hit-testing priority**: characters → objects → exits → walkable floor. ## 6. User interaction - **Verb then target** (SCUMM): select verb, click hotspot. No verb selected: click walks (or fires `defaultVerb` if the spec sets one). - **Keyboard**: number keys 1–9 pick verbs, Esc clears the verb, Space skips scenes. - **Dialogue squares**: choices render as buttons; hidden by conditions. - **Touch**: single taps = clicks; works as-is via canvas pointer events. - **The engine always answers**: unmatched interactions fall back to the verb's default response. Dead air is a spec bug, not an engine bug. ## 7. Save / load Saves are plain JSON strings (`SAVE_VERSION = 1`, game-name tagged, timestamped) — the web layer parks them in `localStorage`; the core never touches storage. Loading rejects: bad JSON, missing/future version, wrong game. Migration hooks exist for format changes. Mid-dialogue saves resume at the saved node. Saving is gated off mid-dialogue/mid-scene (`canSave`). ## 8. Assets & audio — integration only - **Asset manifest** in the spec: `{id, file, frames?, framesPerRow?, framesRows?}`. The engine references ids; the web layer loads files. Missing files → labelled placeholder + one warning. How files get made is out of scope (that's the ComfyUI / midi-playlist side, later). - **Music carries loop points** — `loopStartMs` / `loopEndMs` per track, mirroring the MIDI repo's loop-marker convention. The web audio layer plays the intro head once, then loops the body forever. The *Moonlit Mangrove Calypso* track (bars 4–36 loop) is the reference example: `example/game.json` ships it with loop points set. - **Formats**: images png/jpg/webp; audio ogg/mp3 (anything WebAudio decodes). Sprites are horizontal/row grids; frame timing per anim. ## 9. Testing policy `node --test tests/*.test.js` — 68 tests, one file per module: conditions, effects, walkable (incl. doorway traversal + unreachable areas), loader (example validates clean; every error class reported in one pass; dangling refs; symmetry warnings), verbs (specificity, priority, condition fallthrough, with-items), dialogue (branching, timed auto-advance, hidden choices, mid-dialogue snapshots), save (round-trip, version/game rejection, resume), engine end-to-end (intro scene on the clock, walking, clamping, take/give chains, dialogue gating, endGame), render (manifest degradation, frame cycling, fake-canvas draw ops, HUD model). `./tools/run_all.sh` = validate example + full suite. Green or it didn't happen. ## 10. Conventions - Coordinates: 640×480 room space, y-down, sprites anchored bottom-centre. - Interaction precedence is spec order at equal specificity — put the conditional variant **before** the unconditional one. - Dialogue nodes are named strings, not indices. - Save format changes require bumping `SAVE_VERSION` + a migration. ## 11. Roadmap (deliberately not in v0.1) 1. Game idea + writing (the fun part, next session). 2. Asset generation pipeline: ComfyUI for backgrounds/sprites, midi-playlist → ogg for music, wired into the manifest. 3. Sprite animation polish (facing, walk cycles), character `animate`. 4. Multiple save slots, options menu, gamepad. 5. Room transitions with fades; parallax backgrounds.