Add the updated manual and a teaching game: The DJ's Lost Fuse
- MANUAL.md brought up to the workspace era: game folders + manifest, flags & gated puzzles, hidden dialogue choices, audio file overrides, a Programmatic control section (CLI/HTTP/MCP/Python), and the new two-crate architecture map. - games/lost-fuse: three rooms that demonstrate every mechanic in one chain — item, needs-lock, object sets_flag, a dialogue choice hidden behind requires_flag, dialogue sets_flag, and a flag-locked wins finale. Verified winnable headlessly, gate included. - games/lost-fuse/expansion/roof.jsonl: a live lore drop that upserts a fourth room into a running game and rewires an exit — the "drop JSON lore in" workflow, runnable as a one-liner. - games/README.md: the guided tour — mechanic-to-JSON map, the live expansion demo, and how to prompt an LLM (or point Claude's MCP at the engine) to write valid lore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
211
MANUAL.md
@ -26,10 +26,11 @@ This manual is exhaustive. Jump around with the table of contents.
|
||||
13. [Sound & music](#13-sound--music)
|
||||
14. [The art / sprite pipeline](#14-the-art--sprite-pipeline)
|
||||
15. [The kit & theme packs](#15-the-kit--theme-packs)
|
||||
16. [Room file format (full schema)](#16-room-file-format-full-schema)
|
||||
17. [Project layout & architecture](#17-project-layout--architecture)
|
||||
18. [Troubleshooting](#18-troubleshooting)
|
||||
19. [Roadmap & known gaps](#19-roadmap--known-gaps)
|
||||
16. [Games as folders & the file formats](#16-games-as-folders--the-file-formats)
|
||||
17. [Programmatic control (CLI · HTTP · MCP · Python)](#17-programmatic-control-cli--http--mcp--python)
|
||||
18. [Project layout & architecture](#18-project-layout--architecture)
|
||||
19. [Troubleshooting](#19-troubleshooting)
|
||||
20. [Roadmap & known gaps](#20-roadmap--known-gaps)
|
||||
|
||||
---
|
||||
|
||||
@ -46,11 +47,15 @@ keeps that soul and modernizes everything around it:
|
||||
- **Objects** with descriptions, locks, keys, and win conditions — no code.
|
||||
- **A forgiving parser** + an **optional local LLM** that understands plain English.
|
||||
- **Branching dialogue**, **chiptune SFX**, and **ambient room music**.
|
||||
- **Flags & gated puzzles** — multi-step quests authored as plain JSON.
|
||||
- **A content kit**: 61 click-to-place archetypes across fantasy, sci-fi,
|
||||
monsters, "saucy" nightlife, and classic horror.
|
||||
- **A fully headless core** — the same engine is drivable from the command
|
||||
line, HTTP, Python, or an AI over MCP, with no window (§17).
|
||||
|
||||
Everything saves to small, human-readable JSON. The whole thing is one native
|
||||
binary built with `cargo`.
|
||||
Everything saves to small, human-readable JSON: **a game is a folder** you can
|
||||
zip, share, or have an LLM write. The engine builds with `cargo` into two
|
||||
binaries: the GUI (`mrpgi`) and the headless multi-tool (`mrpgi-headless`).
|
||||
|
||||
---
|
||||
|
||||
@ -73,21 +78,29 @@ are seconds.
|
||||
## 3. Running it
|
||||
|
||||
```sh
|
||||
cargo run # build + run (dev)
|
||||
cargo run --release # optimized build (smoother), slower first compile
|
||||
cargo run -p mrpgi # build + run the GUI (dev)
|
||||
cargo run -p mrpgi --release # optimized build (smoother), slower first compile
|
||||
./run.sh # convenience launcher that points the AI lane at your model
|
||||
```
|
||||
|
||||
### Launch flags
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| *(none)* | Loads `rooms/room0.json` and opens in the **editor**. |
|
||||
| `--sample` | Writes a 3-room "escape the party" demo to `rooms/` then runs it (existing rooms backed up to `*.json.bak`). |
|
||||
| `--kit` | Generates the fantasy starter kit + placeholder sprites, and installs a 4-room fantasy world to `rooms/` (existing → `.bak`). |
|
||||
| *(none)* | Loads the game in the current directory (`game.json` + `rooms/room0.json`) and opens in the **editor**. |
|
||||
| `--game DIR` | Load a game folder instead of the current directory (see §16). |
|
||||
| `--sample` | Writes a 3-room "escape the party" demo (+ `game.json`) then runs it (existing rooms backed up to `*.json.bak`). |
|
||||
| `--kit` | Generates the fantasy starter kit + placeholder sprites, and installs a 4-room fantasy world (existing → `.bak`). |
|
||||
|
||||
```sh
|
||||
cargo run -- --kit # install + play the fantasy starter world
|
||||
cargo run -- --sample # install + play the robot escape demo
|
||||
cargo run -p mrpgi -- --kit # install + play the fantasy starter world
|
||||
cargo run -p mrpgi -- --sample # install + play the robot escape demo
|
||||
cargo run -p mrpgi -- --game games/lost-fuse # play the example game (§16)
|
||||
```
|
||||
|
||||
The headless binary takes the same `--game` / `--sample` / `--kit` flags:
|
||||
|
||||
```sh
|
||||
cargo run -p mrpgi-core --bin mrpgi-headless -- --sample --game games/demo
|
||||
```
|
||||
|
||||
### AI environment variables
|
||||
@ -262,7 +275,9 @@ Objects are sprites you can interact with. Each carries:
|
||||
- **use text** (shown on `use`/`open`),
|
||||
- **needs** — an item that must be in your inventory to `use` it,
|
||||
- **wins** — using it successfully ends the game with a victory banner,
|
||||
- **takeable** — whether `take` adds it to your inventory.
|
||||
- **takeable** — whether `take` adds it to your inventory,
|
||||
- **requires_flag** — a flag that must be true before `use` works,
|
||||
- **sets_flag** — a flag set true when `use` succeeds.
|
||||
|
||||
This is enough for the classic adventure loop **with no code**:
|
||||
|
||||
@ -270,8 +285,23 @@ This is enough for the classic adventure loop **with no code**:
|
||||
> **use text** = *"The gate grinds open…"*, **wins = on**. Now: `take key` →
|
||||
> travel → `use gate` → **win**.
|
||||
|
||||
### Flags — multi-step quests
|
||||
|
||||
**Flags** are named booleans that live for one play session (a game's
|
||||
`game.json` can preset them). Objects and dialogue choices read and write them:
|
||||
|
||||
> A `lever` with **sets_flag = power_on**. A `door` with **requires_flag =
|
||||
> power_on**, **wins = on**. Now `use door` says *"Nothing happens. Something
|
||||
> else must come first."* until you `use lever`.
|
||||
|
||||
Chain `needs` (items) with `requires_flag`/`sets_flag` (state) and gated
|
||||
dialogue (§12) and you get real multi-step quests — all data, no code. The
|
||||
example game `games/lost-fuse/` demonstrates the whole chain (§16).
|
||||
|
||||
Taking an object removes it from the room (runtime only — it returns on a fresh
|
||||
play). Objects do not block movement (walls come from the painted meaning layer).
|
||||
Currently `requires_flag`/`sets_flag` are JSON-authored (the editor inspector
|
||||
shows the older fields only).
|
||||
|
||||
---
|
||||
|
||||
@ -343,12 +373,28 @@ planned). Each entry is a **node**; node 0 is the entry. See the inn barkeep in
|
||||
A choice's **`goto`** is the next node index, or **`-1`** to end. A node with no
|
||||
choices is a terminal line (shown, then the conversation ends).
|
||||
|
||||
### Gated choices
|
||||
|
||||
Choices can read and write flags (§9), which makes conversations part of the
|
||||
puzzle chain:
|
||||
|
||||
```json
|
||||
{ "text": "Hear that? I'm with the band.", "goto": 2,
|
||||
"requires_flag": "music_on", "sets_flag": "on_the_list" }
|
||||
```
|
||||
|
||||
A choice with `requires_flag` is **hidden** until that flag is true — the
|
||||
numbering the player sees always matches what they can pick. `sets_flag` fires
|
||||
when the choice is chosen. So an NPC can refuse to say something until you've
|
||||
done a thing, and telling them can unlock a door elsewhere.
|
||||
|
||||
---
|
||||
|
||||
## 13. Sound & music
|
||||
|
||||
All audio is **synthesized at runtime** (square waves → PCM, no files), in the
|
||||
spirit of AGI's PCjr beeper.
|
||||
By default all audio is **synthesized at runtime** (square waves → PCM, no
|
||||
files), in the spirit of AGI's PCjr beeper — and any of it can be **overridden
|
||||
per game with real audio files** (below).
|
||||
|
||||
**SFX (play mode):** a blip when you submit a command, a ding on `take`, a chime on
|
||||
a successful `use`/`look`, a buzz when nothing matches, a whoosh on room change, a
|
||||
@ -361,6 +407,18 @@ It plays in play mode and switches as you move between rooms.
|
||||
**`F2`** mutes music and SFX together. If a machine has no audio backend, everything
|
||||
degrades to silence.
|
||||
|
||||
### Your own audio files
|
||||
|
||||
Drop files into the game folder and they replace the synth, cue for cue:
|
||||
|
||||
```
|
||||
<game>/sfx/blip|pickup|confirm|error|door|win .wav|.ogg|.mp3
|
||||
<game>/music/calm|eerie|tense|jolly|spooky .wav|.ogg|.mp3 (or 1.ogg … 5.ogg)
|
||||
```
|
||||
|
||||
Anything you don't provide keeps its chiptune default. Prefer **OGG for
|
||||
looping music** (MP3 encoders pad silence that clicks at the loop seam).
|
||||
|
||||
---
|
||||
|
||||
## 14. The art / sprite pipeline
|
||||
@ -409,7 +467,41 @@ then wire its exits in the editor.
|
||||
|
||||
---
|
||||
|
||||
## 16. Room file format (full schema)
|
||||
## 16. Games as folders & the file formats
|
||||
|
||||
**A game is a folder.** Zip it, share it, generate it with an LLM — the engine
|
||||
loads it with `--game DIR` (both binaries):
|
||||
|
||||
```
|
||||
games/mygame/
|
||||
game.json the manifest (below) — optional, every field defaults
|
||||
rooms/room0.json one RoomDoc per room; start room comes from the manifest
|
||||
sprites/*.png auto-quantized to EGA; filename = sprite name
|
||||
sfx/ music/ optional audio-file overrides (§13)
|
||||
```
|
||||
|
||||
A bare directory with just `rooms/` is already a valid game — that's how
|
||||
legacy projects keep loading.
|
||||
|
||||
**game.json (the manifest):**
|
||||
```json
|
||||
{
|
||||
"name": "The DJ's Lost Fuse",
|
||||
"start_room": 0,
|
||||
"intro_text": "The party upstairs went quiet an hour ago. Type 'help'.",
|
||||
"verbs": [ { "name": "use", "words": ["plug", "spin"] } ],
|
||||
"flags": { "met_bouncer": false }
|
||||
}
|
||||
```
|
||||
`verbs` merges extra synonyms into the built-in verb table (the AI lane's
|
||||
whitelist is generated from the same table, so they can't drift). `flags` are
|
||||
the starting flag values for each new game.
|
||||
|
||||
A complete worked example ships in **`games/lost-fuse/`** — three rooms
|
||||
demonstrating every mechanic (items, locks, flags, gated dialogue, win), with
|
||||
a guided tour in [`games/README.md`](games/README.md).
|
||||
|
||||
### Room file format (full schema)
|
||||
|
||||
A room is a `RoomDoc` serialized as JSON:
|
||||
|
||||
@ -442,11 +534,13 @@ A room is a `RoomDoc` serialized as JSON:
|
||||
"look": "A heavy gate.", "takeable": false,
|
||||
"synonyms": "door portcullis", "use_text": "It grinds open!",
|
||||
"needs": "key", "wins": true,
|
||||
"requires_flag": "power_on", "sets_flag": "gate_open",
|
||||
"dialogue": [ <DlgNode>, ... ]
|
||||
}
|
||||
```
|
||||
**DlgNode:** `{ "says": "...", "choices": [ { "text": "...", "goto": 1 } ] }` —
|
||||
`goto` is a node index or `-1` to end.
|
||||
**DlgNode:** `{ "says": "...", "choices": [ { "text": "...", "goto": 1,
|
||||
"requires_flag": "", "sets_flag": "" } ] }` — `goto` is a node index or `-1`
|
||||
to end; the flag fields are optional (§12).
|
||||
|
||||
All fields except `name`/`sprite`/`x`/`y` default if omitted, so hand-written JSON
|
||||
can be minimal. Strokes are *replayed* into the buffers on load, so rooms stay tiny
|
||||
@ -454,36 +548,71 @@ and re-editable.
|
||||
|
||||
---
|
||||
|
||||
## 17. Project layout & architecture
|
||||
## 17. Programmatic control (CLI · HTTP · MCP · Python)
|
||||
|
||||
The engine core is fully headless: a `GameState` driven by JSON `Command`s in
|
||||
and `Event`s out. The GUI is just one consumer of that bus — the same bus is
|
||||
exposed as:
|
||||
|
||||
- **JSONL stdio** — `mrpgi-headless --game DIR`; pipe
|
||||
`{"cmd":"parse","text":"take key"}` lines in, events stream out.
|
||||
- **Script replay** — `--script play.jsonl` replays a command log
|
||||
deterministically (byte-identical transcripts; the CI golden-test workflow).
|
||||
- **Headless render** — `--render-room rooms/room0.json out.png` bakes any
|
||||
room to a PNG with no window.
|
||||
- **HTTP** — `--serve 127.0.0.1:7777`: `POST /command`, `GET /state`,
|
||||
`GET /frame.png`, `GET /events?since=N`, `POST /rooms/{n}`. Python drives it
|
||||
with plain `requests`.
|
||||
- **MCP** — `--mcp`: a Model Context Protocol server, so Claude (or any MCP
|
||||
client) can both *play* the game and *author* it — including
|
||||
`mrpgi_render_frame`, which returns a real PNG so the model sees its work:
|
||||
`claude mcp add mrpgi -- /path/to/mrpgi-headless --mcp --game games/mygame`
|
||||
|
||||
Full command/event reference and authoring contract:
|
||||
**[docs/CONTROL.md](docs/CONTROL.md)**.
|
||||
|
||||
---
|
||||
|
||||
## 18. Project layout & architecture
|
||||
|
||||
Two crates in one workspace:
|
||||
|
||||
```
|
||||
src/
|
||||
main.rs window, game loop, modes, parser, dialogue runtime, compositing
|
||||
palette.rs the 16-colour EGA palette (index → RGBA)
|
||||
framebuffer.rs visual + priority buffers, the band(y) depth table
|
||||
picture.rs drawing primitives (lines, rects, flood-fill, ellipse, stamps)
|
||||
view.rs sprites as cels; ASCII-grid + image cel builders
|
||||
sprite.rs the ego + props: movement, collision, walk-cycle
|
||||
assets.rs PNG load + EGA quantize/dither; placeholder sprite generation
|
||||
editor.rs the whole editor: tools, panel, RoomDoc, objects, kit, save/load
|
||||
ai.rs the AI parser lane (Ollama / OpenRouter, on a worker thread)
|
||||
sound.rs chiptune SFX + ambient music (square-wave WAV synthesis)
|
||||
room.rs legacy demo-room helper
|
||||
mrpgi-core/ the headless engine (ZERO window/audio deps — enforced)
|
||||
src/state.rs GameState + the Command/Event bus + fixed-step tick
|
||||
src/world.rs RoomDoc/ObjDef/dialogue data model, World, game folders
|
||||
src/parser.rs data-driven verb table, verb-noun parser, dialogue state
|
||||
src/render.rs headless compositing → RGBA / PNG
|
||||
src/framebuffer.rs visual + priority buffers, the band(y) depth table
|
||||
src/picture.rs drawing primitives (lines, rects, flood-fill, stamps)
|
||||
src/view.rs sprites as cels; ASCII-grid cel builder
|
||||
src/sprite.rs the ego + props: movement, collision, walk-cycle
|
||||
src/assets.rs PNG load + EGA quantize/dither; placeholder sprites
|
||||
src/audio.rs chiptune synthesis + audio cues (content, not output)
|
||||
src/ai.rs the AI parser lane (Ollama / OpenRouter, worker thread)
|
||||
src/kit.rs sample game + starter kit writers
|
||||
src/bin/mrpgi-headless/ the CLI/HTTP/MCP surfaces
|
||||
tests/engine.rs parser goldens + a full win-path replay test
|
||||
mrpgi/ the macroquad GUI (one consumer of the bus)
|
||||
src/main.rs window, play loop over Commands/Events, HUD
|
||||
src/editor.rs the paint tool (commits edits through the same bus)
|
||||
src/sound.rs audio out: game-folder files override the synth
|
||||
kit/ the archetype/theme library + art manifesto
|
||||
sprites/ sprite PNGs (placeholders + your art)
|
||||
rooms/ your world (room0.json …)
|
||||
games/ game folders (lost-fuse example lives here)
|
||||
sprites/ rooms/ the legacy in-place game (still loads fine)
|
||||
palettes/ the EGA palette for Aseprite/Inkscape (.gpl, .hex)
|
||||
```
|
||||
|
||||
**Rendering:** rooms rasterize into a 160×168 visual buffer + a priority buffer.
|
||||
Each frame, the ego and props are drawn back-to-front by depth; a sprite pixel is
|
||||
only drawn where `sprite_priority ≥ scenery_priority` (the walk-behind trick). The
|
||||
result is uploaded to a GPU texture and scaled crisp to the window. A fixed-step
|
||||
"cycle" (20/s) drives movement, decoupled from frame rate.
|
||||
result is composited headlessly in the core; the GUI just uploads it to a GPU
|
||||
texture. A fixed-step "cycle" (20/s) drives movement, decoupled from frame
|
||||
rate — which is why scripted replays are bit-identical.
|
||||
|
||||
---
|
||||
|
||||
## 18. Troubleshooting
|
||||
## 19. Troubleshooting
|
||||
|
||||
- **Objects are little magenta squares** → that sprite PNG doesn't exist yet in
|
||||
`sprites/`. Make it (see §14) or it's just a placeholder; the object still works.
|
||||
@ -501,13 +630,17 @@ result is uploaded to a GPU texture and scaled crisp to the window. A fixed-step
|
||||
|
||||
---
|
||||
|
||||
## 19. Roadmap & known gaps
|
||||
## 20. Roadmap & known gaps
|
||||
|
||||
- In-editor **dialogue-tree builder** (dialogue is JSON-authored for now).
|
||||
- In-editor **dialogue-tree builder** and flag-field editing (both are
|
||||
JSON-authored for now; the `dialogue`/`requires_flag`/`sets_flag` fields
|
||||
work — the inspector just doesn't show them yet).
|
||||
- The **kit-browser villager** has no built-in dialogue yet (the live inn one does).
|
||||
- More theme packs; **title screen / world picker**; **save/load a playthrough**.
|
||||
- Water/trigger control lines are reserved but not yet wired to behaviour.
|
||||
- True-colour ("HD") background layer and polygon fill regions.
|
||||
- **Strudel** live-coded music as a browser sidecar over the HTTP surface.
|
||||
- Pathfinding for click-to-walk (today: greedy steering); cross-room NPC state.
|
||||
|
||||
---
|
||||
|
||||
|
||||
87
games/README.md
Normal file
@ -0,0 +1,87 @@
|
||||
# Games are folders. Lore is JSON.
|
||||
|
||||
Every MRPGI game is a folder you can zip, share, hand-write, or have an LLM
|
||||
generate. This directory holds examples; **`lost-fuse/`** is the guided one —
|
||||
three rooms that demonstrate every mechanic the engine has, in one small
|
||||
puzzle chain.
|
||||
|
||||
## Play it
|
||||
|
||||
```sh
|
||||
cargo run -p mrpgi -- --game games/lost-fuse # in the GUI
|
||||
cargo run -p mrpgi-core --bin mrpgi-headless -- --game games/lost-fuse # in a terminal, JSON in/out
|
||||
```
|
||||
|
||||
*(Spoilers below — play it first! It's five minutes.)*
|
||||
|
||||
## The anatomy of a quest
|
||||
|
||||
The whole game is ~200 lines of JSON. The chain:
|
||||
|
||||
```
|
||||
take fuse an item → inventory (room 0)
|
||||
use amp needs: fuse → sets_flag music_on (room 1)
|
||||
talk bouncer a choice gated by requires_flag music_on
|
||||
"I fixed your amp." → sets_flag on_the_list
|
||||
use turntable requires_flag on_the_list → wins: true (room 2)
|
||||
```
|
||||
|
||||
Each step is one field on an object or dialogue choice — no code anywhere:
|
||||
|
||||
| Mechanic | Where to look |
|
||||
|---|---|
|
||||
| A takeable item | `fuse` in [`rooms/room0.json`](lost-fuse/rooms/room0.json) |
|
||||
| An item lock (`needs`) | `amp` in [`rooms/room1.json`](lost-fuse/rooms/room1.json) |
|
||||
| An object setting a flag (`sets_flag`) | same `amp` |
|
||||
| A **hidden** dialogue choice (`requires_flag`) | the bouncer's second choice — try talking to it before and after fixing the amp |
|
||||
| Dialogue setting a flag | same choice (`sets_flag: on_the_list`) |
|
||||
| A flag-locked finale (`requires_flag` + `wins`) | `turntable` in [`rooms/room2.json`](lost-fuse/rooms/room2.json) |
|
||||
| Rooms linked by edges (`exits`) | every room: `[N, E, S, W]` room numbers |
|
||||
| Painted look + walkability (`strokes`) | every room: `Rect`s with `meaning` `Floor`/`Wall`/`None` |
|
||||
| Per-room music moods | `"music": 2/3/4` (eerie street → tense door → jolly club) |
|
||||
| Manifest (name, intro, extra verb synonyms) | [`game.json`](lost-fuse/game.json) — try typing `fix amp` |
|
||||
|
||||
## Dropping lore into a RUNNING game
|
||||
|
||||
Rooms can be added live over any control surface — this is the "drop JSON
|
||||
lore in" workflow. [`lost-fuse/expansion/roof.jsonl`](lost-fuse/expansion/roof.jsonl)
|
||||
adds a roof afterparty above the club:
|
||||
|
||||
```sh
|
||||
target/debug/mrpgi-headless --script games/lost-fuse/expansion/roof.jsonl --game games/lost-fuse
|
||||
```
|
||||
|
||||
It upserts room 3 (validated: a room with no walkable spawn, or exits pointing
|
||||
at rooms that don't exist, is rejected with an `error` event), then wires
|
||||
room 2's north exit to it. The same two commands work over HTTP
|
||||
(`POST /rooms/3`, `POST /command`) or MCP (`mrpgi_upsert_room`) against a live
|
||||
session.
|
||||
|
||||
## Asking an LLM for lore
|
||||
|
||||
The formats are plain serde types, so generated JSON either loads or fails
|
||||
loudly. A prompt that works well:
|
||||
|
||||
> Here is a room from my MRPGI game: *(paste a room JSON)*. Write a new room
|
||||
> in exactly this format: *(describe the room, its objects, its puzzle role,
|
||||
> which exits connect where)*. Rooms are 160×168; sprites available: altar,
|
||||
> barrel, chest, key, orc, sign, villager. Only use `requires_flag`/`sets_flag`
|
||||
> names that exist in my game.
|
||||
|
||||
Or skip the copy-paste entirely: register the MCP server
|
||||
(`claude mcp add mrpgi -- mrpgi-headless --mcp --game games/lost-fuse`) and
|
||||
the model can upsert rooms, render frames to *see* them, and playtest its own
|
||||
puzzle chain. Full command reference: [../docs/CONTROL.md](../docs/CONTROL.md).
|
||||
|
||||
## Make your own
|
||||
|
||||
```sh
|
||||
mkdir -p games/mygame
|
||||
cargo run -p mrpgi-core --bin mrpgi-headless -- --sample --game games/mygame # a starting skeleton
|
||||
cargo run -p mrpgi -- --game games/mygame # paint it in the editor
|
||||
```
|
||||
|
||||
Sprites: drop PNGs in `games/mygame/sprites/` (auto-quantized to EGA, alpha =
|
||||
transparent, bottom edge = feet). Audio: drop files in `sfx/` / `music/` to
|
||||
replace the chiptune synth (see MANUAL.md §13). Everything else is the JSON
|
||||
you just read.
|
||||
7
games/lost-fuse/expansion/roof.jsonl
Normal file
@ -0,0 +1,7 @@
|
||||
# Lore drop: adds a Roof Afterparty above the club, LIVE, while the game runs.
|
||||
# Try it: target/debug/mrpgi-headless --script games/lost-fuse/expansion/roof.jsonl --game games/lost-fuse
|
||||
# (Add "persist":true / use save_room to keep it on disk.)
|
||||
{"cmd":"upsert_room","n":3,"doc":{"strokes":[{"Rect":{"x":0,"y":0,"w":160,"h":168,"color":1,"meaning":"None"}},{"Rect":{"x":6,"y":16,"w":148,"h":146,"color":8,"meaning":"Floor"}},{"Rect":{"x":0,"y":0,"w":160,"h":16,"color":0,"meaning":"Wall"}},{"Rect":{"x":0,"y":160,"w":65,"h":8,"color":0,"meaning":"Wall"}},{"Rect":{"x":95,"y":160,"w":65,"h":8,"color":0,"meaning":"Wall"}},{"Rect":{"x":0,"y":0,"w":6,"h":168,"color":0,"meaning":"Wall"}},{"Rect":{"x":154,"y":0,"w":6,"h":168,"color":0,"meaning":"Wall"}},{"Ellipse":{"x":20,"y":2,"w":14,"h":10,"color":14,"meaning":"None"}},{"Rect":{"x":30,"y":30,"w":100,"h":4,"color":13,"meaning":"None"}}],"spawn":[80,144],"exits":[null,null,2,null],"objects":[{"name":"punch","sprite":"barrel","x":40,"y":126,"look":"Afterparty punch. It is fizzing in a way liquids should not.","synonyms":"drink barrel"},{"name":"skyline","sprite":"sign","x":110,"y":60,"look":"The whole neon city, humming below. Worth saving a party for.","synonyms":"city view"}],"music":1}}
|
||||
{"cmd":"goto_room","n":2}
|
||||
{"cmd":"set_exit","dir":0,"target":3}
|
||||
{"cmd":"query"}
|
||||
10
games/lost-fuse/game.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "The DJ's Lost Fuse",
|
||||
"start_room": 0,
|
||||
"intro_text": "Night. A scrapyard. Somewhere above, a party has gone horribly silent. Type 'help'.",
|
||||
"verbs": [
|
||||
{ "name": "use", "words": ["plug", "fix", "repair"] },
|
||||
{ "name": "look", "words": ["scope"] }
|
||||
],
|
||||
"flags": {}
|
||||
}
|
||||
64
games/lost-fuse/rooms/room0.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"strokes": [
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 168, "color": 1, "meaning": "None" } },
|
||||
{ "Rect": { "x": 6, "y": 16, "w": 148, "h": 146, "color": 8, "meaning": "Floor" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 16, "color": 0, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 160, "w": 160, "h": 8, "color": 0, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 6, "h": 168, "color": 0, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 154, "y": 0, "w": 6, "h": 70, "color": 0, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 154, "y": 100, "w": 6, "h": 68, "color": 0, "meaning": "Wall" } },
|
||||
{ "Ellipse": { "x": 118, "y": 3, "w": 16, "h": 11, "color": 14, "meaning": "None" } },
|
||||
{ "Ellipse": { "x": 22, "y": 104, "w": 34, "h": 18, "color": 6, "meaning": "None" } },
|
||||
{ "Ellipse": { "x": 40, "y": 112, "w": 22, "h": 12, "color": 7, "meaning": "None" } },
|
||||
{ "Rect": { "x": 130, "y": 72, "w": 24, "h": 4, "color": 10, "meaning": "None" } }
|
||||
],
|
||||
"spawn": [30, 140],
|
||||
"exits": [null, 1, null, null],
|
||||
"objects": [
|
||||
{
|
||||
"name": "fuse",
|
||||
"sprite": "key",
|
||||
"x": 52,
|
||||
"y": 128,
|
||||
"look": "A chunky glass fuse, pried from a dead washing machine. Miraculously intact.",
|
||||
"takeable": true,
|
||||
"synonyms": "glass part spare"
|
||||
},
|
||||
{
|
||||
"name": "scrap-bot",
|
||||
"sprite": "villager",
|
||||
"x": 104,
|
||||
"y": 122,
|
||||
"look": "A retired greeter-bot, rusting politely among the junk. Its smile LED still works.",
|
||||
"synonyms": "robot greeter bot",
|
||||
"use_text": "You pat the scrap-bot. It emits a small, grateful beep.",
|
||||
"dialogue": [
|
||||
{
|
||||
"says": "The club went quiet an hour ago. Bad scene. A party without bass is just... standing.",
|
||||
"choices": [
|
||||
{ "text": "What happened?", "goto": 1 },
|
||||
{ "text": "Who are you?", "goto": 2 },
|
||||
{ "text": "Later.", "goto": -1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"says": "The big amp blew its fuse mid-drop. The DJ is up there crying in binary. There are spare parts all over this yard, friend.",
|
||||
"choices": [{ "text": "Say less.", "goto": -1 }]
|
||||
},
|
||||
{
|
||||
"says": "Chassis No. 7. I greet, therefore I am.",
|
||||
"choices": [{ "text": "Deep.", "goto": -1 }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sign",
|
||||
"sprite": "sign",
|
||||
"x": 78,
|
||||
"y": 52,
|
||||
"look": "Spray-painted: CLUB VOLTAGE ->-> (through the gap in the east fence).",
|
||||
"synonyms": "graffiti notice"
|
||||
}
|
||||
],
|
||||
"music": 2
|
||||
}
|
||||
64
games/lost-fuse/rooms/room1.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"strokes": [
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 168, "color": 0, "meaning": "None" } },
|
||||
{ "Rect": { "x": 6, "y": 16, "w": 148, "h": 146, "color": 7, "meaning": "Floor" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 65, "h": 16, "color": 8, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 95, "y": 0, "w": 65, "h": 16, "color": 8, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 160, "w": 160, "h": 8, "color": 8, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 6, "h": 70, "color": 8, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 100, "w": 6, "h": 68, "color": 8, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 154, "y": 0, "w": 6, "h": 168, "color": 8, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 66, "y": 4, "w": 28, "h": 12, "color": 5, "meaning": "None" } },
|
||||
{ "Rect": { "x": 14, "y": 24, "w": 34, "h": 8, "color": 13, "meaning": "None" } },
|
||||
{ "Rect": { "x": 112, "y": 22, "w": 34, "h": 8, "color": 11, "meaning": "None" } },
|
||||
{ "Rect": { "x": 60, "y": 30, "w": 40, "h": 6, "color": 14, "meaning": "None" } }
|
||||
],
|
||||
"spawn": [80, 140],
|
||||
"exits": [2, null, null, 0],
|
||||
"objects": [
|
||||
{
|
||||
"name": "amp",
|
||||
"sprite": "chest",
|
||||
"x": 122,
|
||||
"y": 132,
|
||||
"look": "The club's outdoor amp, tall as a fridge. Its fuse slot gapes empty and scorched.",
|
||||
"synonyms": "amplifier speaker stack",
|
||||
"use_text": "You slot the fuse home. The amp THUMPS alive - bass rolls down the street like weather.",
|
||||
"needs": "fuse",
|
||||
"sets_flag": "music_on"
|
||||
},
|
||||
{
|
||||
"name": "bouncer",
|
||||
"sprite": "orc",
|
||||
"x": 80,
|
||||
"y": 108,
|
||||
"look": "A slab of security chrome parked in front of the club door. Its arms are crossed so hard they have fused.",
|
||||
"synonyms": "guard security doorbot",
|
||||
"use_text": "You push the bouncer. The bouncer does not notice.",
|
||||
"dialogue": [
|
||||
{
|
||||
"says": "List. Name. No name, no party.",
|
||||
"choices": [
|
||||
{ "text": "C'mon, one dance?", "goto": 1 },
|
||||
{
|
||||
"text": "Hear that bass? I fixed your amp. I'm with the band.",
|
||||
"goto": 2,
|
||||
"requires_flag": "music_on",
|
||||
"sets_flag": "on_the_list"
|
||||
},
|
||||
{ "text": "Fine.", "goto": -1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"says": "No list, no dance. The list is life.",
|
||||
"choices": [{ "text": "Harsh.", "goto": -1 }]
|
||||
},
|
||||
{
|
||||
"says": "...That WAS you? Respect, fixer. You're on the list. The booth is upstairs, north door.",
|
||||
"choices": [{ "text": "Let's go.", "goto": -1 }]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"music": 3
|
||||
}
|
||||
47
games/lost-fuse/rooms/room2.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"strokes": [
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 168, "color": 0, "meaning": "None" } },
|
||||
{ "Rect": { "x": 6, "y": 16, "w": 148, "h": 146, "color": 8, "meaning": "Floor" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 16, "color": 0, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 160, "w": 65, "h": 8, "color": 0, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 95, "y": 160, "w": 65, "h": 8, "color": 0, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 6, "h": 168, "color": 0, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 154, "y": 0, "w": 6, "h": 168, "color": 0, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 40, "y": 96, "w": 80, "h": 52, "color": 5, "meaning": "None" } },
|
||||
{ "Rect": { "x": 56, "y": 108, "w": 48, "h": 28, "color": 13, "meaning": "None" } },
|
||||
{ "Rect": { "x": 12, "y": 24, "w": 30, "h": 8, "color": 11, "meaning": "None" } },
|
||||
{ "Rect": { "x": 118, "y": 24, "w": 30, "h": 8, "color": 13, "meaning": "None" } },
|
||||
{ "Rect": { "x": 58, "y": 8, "w": 44, "h": 10, "color": 14, "meaning": "None" } },
|
||||
{ "Rect": { "x": 52, "y": 62, "w": 56, "h": 12, "color": 7, "meaning": "None" } }
|
||||
],
|
||||
"spawn": [80, 144],
|
||||
"exits": [null, null, 1, null],
|
||||
"objects": [
|
||||
{
|
||||
"name": "turntable",
|
||||
"sprite": "altar",
|
||||
"x": 80,
|
||||
"y": 72,
|
||||
"look": "The decks. A crown for whoever dares. Below, three hundred robots hold their breath.",
|
||||
"synonyms": "decks booth dj deck",
|
||||
"use_text": "You drop the needle. The building inhales - then the beat lands and three hundred robots lose their minds. You are the DJ now. THE PARTY IS SAVED.",
|
||||
"requires_flag": "on_the_list",
|
||||
"wins": true
|
||||
},
|
||||
{
|
||||
"name": "dj",
|
||||
"sprite": "villager",
|
||||
"x": 36,
|
||||
"y": 112,
|
||||
"look": "The resident DJ, head in hands, humming quiet error tones.",
|
||||
"synonyms": "deejay resident",
|
||||
"dialogue": [
|
||||
{
|
||||
"says": "My set... my fuse... my LIFE...",
|
||||
"choices": [{ "text": "I got you. Watch this.", "goto": -1 }]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"music": 4
|
||||
}
|
||||
BIN
games/lost-fuse/sprites/altar.png
Normal file
|
After Width: | Height: | Size: 202 B |
BIN
games/lost-fuse/sprites/barrel.png
Normal file
|
After Width: | Height: | Size: 266 B |
BIN
games/lost-fuse/sprites/chest.png
Normal file
|
After Width: | Height: | Size: 224 B |
BIN
games/lost-fuse/sprites/key.png
Normal file
|
After Width: | Height: | Size: 201 B |
BIN
games/lost-fuse/sprites/orc.png
Normal file
|
After Width: | Height: | Size: 258 B |
BIN
games/lost-fuse/sprites/sign.png
Normal file
|
After Width: | Height: | Size: 206 B |
BIN
games/lost-fuse/sprites/villager.png
Normal file
|
After Width: | Height: | Size: 269 B |