diff --git a/MANUAL.md b/MANUAL.md index b7d014a..6a1d097 100644 --- a/MANUAL.md +++ b/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 -./run.sh # convenience launcher that points the AI lane at your model +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: + +``` +/sfx/blip|pickup|confirm|error|door|win .wav|.ogg|.mp3 +/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:** `{ "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 -kit/ the archetype/theme library + art manifesto -sprites/ sprite PNGs (placeholders + your art) -rooms/ your world (room0.json …) -palettes/ the EGA palette for Aseprite/Inkscape (.gpl, .hex) +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 +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. --- diff --git a/games/README.md b/games/README.md new file mode 100644 index 0000000..75be1e7 --- /dev/null +++ b/games/README.md @@ -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. diff --git a/games/lost-fuse/expansion/roof.jsonl b/games/lost-fuse/expansion/roof.jsonl new file mode 100644 index 0000000..d2642e3 --- /dev/null +++ b/games/lost-fuse/expansion/roof.jsonl @@ -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"} diff --git a/games/lost-fuse/game.json b/games/lost-fuse/game.json new file mode 100644 index 0000000..1565140 --- /dev/null +++ b/games/lost-fuse/game.json @@ -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": {} +} diff --git a/games/lost-fuse/rooms/room0.json b/games/lost-fuse/rooms/room0.json new file mode 100644 index 0000000..a4e96e9 --- /dev/null +++ b/games/lost-fuse/rooms/room0.json @@ -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 +} diff --git a/games/lost-fuse/rooms/room1.json b/games/lost-fuse/rooms/room1.json new file mode 100644 index 0000000..ec79a4f --- /dev/null +++ b/games/lost-fuse/rooms/room1.json @@ -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 +} diff --git a/games/lost-fuse/rooms/room2.json b/games/lost-fuse/rooms/room2.json new file mode 100644 index 0000000..6fc8571 --- /dev/null +++ b/games/lost-fuse/rooms/room2.json @@ -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 +} diff --git a/games/lost-fuse/sprites/altar.png b/games/lost-fuse/sprites/altar.png new file mode 100644 index 0000000..9792ea8 Binary files /dev/null and b/games/lost-fuse/sprites/altar.png differ diff --git a/games/lost-fuse/sprites/barrel.png b/games/lost-fuse/sprites/barrel.png new file mode 100644 index 0000000..cfc3f5c Binary files /dev/null and b/games/lost-fuse/sprites/barrel.png differ diff --git a/games/lost-fuse/sprites/chest.png b/games/lost-fuse/sprites/chest.png new file mode 100644 index 0000000..6ac3c94 Binary files /dev/null and b/games/lost-fuse/sprites/chest.png differ diff --git a/games/lost-fuse/sprites/key.png b/games/lost-fuse/sprites/key.png new file mode 100644 index 0000000..f56c586 Binary files /dev/null and b/games/lost-fuse/sprites/key.png differ diff --git a/games/lost-fuse/sprites/orc.png b/games/lost-fuse/sprites/orc.png new file mode 100644 index 0000000..04518df Binary files /dev/null and b/games/lost-fuse/sprites/orc.png differ diff --git a/games/lost-fuse/sprites/sign.png b/games/lost-fuse/sprites/sign.png new file mode 100644 index 0000000..ad905e6 Binary files /dev/null and b/games/lost-fuse/sprites/sign.png differ diff --git a/games/lost-fuse/sprites/villager.png b/games/lost-fuse/sprites/villager.png new file mode 100644 index 0000000..dadd75f Binary files /dev/null and b/games/lost-fuse/sprites/villager.png differ