commit d0caf983a01fb9736ac064b53d93d90756067755 Author: type-two Date: Wed Jun 17 17:26:55 2026 +1000 Initial commit: MRPGI — Apple-Silicon AGI adventure engine A new-school reimagining of Sierra's 1980s AGI interpreter in Rust + macroquad: - Dual-buffer (visual + priority) room model with hand-painted walkability - In-engine room editor: brush/line/rect/fill/pick/erase/oval/image/spawn/object tools - Sprite pipeline (PNG -> EGA quantize), multi-room worlds with edge exits - Objects with look/use/needs/keys/win conditions (no code) - Forgiving text parser + optional local-LLM lane (Ollama / OpenRouter) - Branching NPC dialogue trees, chiptune SFX, per-room ambient music - Content kit: 61 archetypes across fantasy/scifi/monsters/saucy/horror - Full docs: README, EDITOR_GUIDE, MANUAL, art manifesto + Gemini prompts Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b9a3e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Rust build output +/target +**/*.rs.bk + +# Editor/runtime backups (regenerable; git is the backup now) +**/*.bak + +# macOS / IDE cruft +.DS_Store +.claude/ + +# Stray test renders that landed here from another project +001.jpeg +002.png +003.png diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4ec0c32 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,883 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fontdue" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" +dependencies = [ + "hashbrown", + "ttf-parser", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glam" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "macroquad" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f7d60318b52b19e909db1f01c522977da28a5e515127a73ecd8b7488498edb" +dependencies = [ + "fontdue", + "glam", + "image", + "macroquad_macro", + "miniquad", + "quad-rand", +] + +[[package]] +name = "macroquad_macro" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b1d96218903768c1ce078b657c0d5965465c95a60d2682fd97443c9d2483dd" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "miniquad" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f64fd94ff70fdc425766c78a3fa9f263d02cb25725a49f255f6a66d8a186c3f" +dependencies = [ + "libc", + "ndk-sys", + "objc-rs", + "winapi", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mrpgi" +version = "0.1.0" +dependencies = [ + "image", + "macroquad", + "serde", + "serde_json", + "ureq", +] + +[[package]] +name = "ndk-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc-rs" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a1e7069a2525126bf12a9f1f7916835fafade384fb27cabf698e745e2a1eb8" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quad-rand" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9c7251e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "mrpgi" +version = "0.1.0" +edition = "2021" +description = "Monster Robot Party Game Interpreter — a new-school, Apple-Silicon-native reimagining of the Sierra AGI adventure engine." + +[dependencies] +macroquad = "0.4" +image = { version = "0.24", default-features = false, features = ["png"] } # PNG-only: lean builds +serde = { version = "1", features = ["derive"] } # room (.json) save/load +serde_json = "1" +ureq = { version = "2", features = ["json", "tls"] } # AI parser: local Ollama / OpenRouter HTTP + +# macroquad renders much more smoothly when its dependencies are optimized, +# even during `cargo run` dev builds. Our own crate stays fast to recompile. +[profile.dev] +opt-level = 1 + +[profile.dev.package."*"] +opt-level = 3 diff --git a/EDITOR_GUIDE.md b/EDITOR_GUIDE.md new file mode 100644 index 0000000..1f9f514 --- /dev/null +++ b/EDITOR_GUIDE.md @@ -0,0 +1,230 @@ +# MRPGI room editor — the dummy's guide + +You draw a room, tag what's walkable, press a key, and walk around it. That's it. + +## Run it + +```sh +cargo run +``` + +It opens in **EDIT** mode. Press **`Tab`** any time to flip between **edit** (draw) and +**play** (walk). `Esc` quits. + +## The one rule (read this twice) + +Every spot on the screen has **two** properties, and you set them separately: + +- **LOOK** = the colour (what you see). You pick it from the **palette**. +- **MEANING** = what it *is* to the game (walkable? a wall?). You pick it from the **meaning** tags. + +So a brown floor and a brown wall look identical but behave differently — because +**colour is just looks; the tag is the meaning.** When you fill or draw a region, +it gets *both*: the current colour **and** the current meaning tag. + +Meaning tags: +- **floor** — walkable (the robot can stand here). Default. +- **wall** — blocked (the robot can't enter). +- **water** / **trigger** — special (reserved for later). +- **none** — paint colour only, don't change walkability. + +## Make your first room (the classic 3-band road) + +1. Press **`Tab`** until the panel says you're editing (you start here). +2. Click the **fill** tool (or press `4`). +3. Click **blue** in the palette, click the **wall** tag, then click the **top third** → blue sky that blocks. +4. Click **green**, keep **wall**, click the **bottom third** → green ground that blocks. +5. Click **brown**, click the **floor** tag, click the **middle band** → a brown road that's walkable. +6. Press **`Tab`** → you're now playing. Use the **arrow keys** — the robot can only walk the road. 🎉 + +Tip: tap **`L`** to cycle the **view** — `both` tints walls red so you can *see* +what's walkable vs blocked. `look` shows the plain picture; `ctrl` shows the meaning map. + +## Tools + +| Tool | Key | What it does | +|------|-----|--------------| +| brush | `1` | freehand paint (colour + meaning) | +| line | `2` | click points; **right-click or Enter** to finish the line | +| rect | `3` | drag a filled box | +| fill | `4` | flood-fill a sealed area (the bucket) | +| pick | `5` | eyedropper — grab a colour off the canvas | +| erase | `6` | rub back to blank + walkable | +| image | `7` | stamp a sprite from `sprites/` (`[` `]` choose, `R` reload) | +| oval | `8` | drag a filled ellipse | +| spawn | `9` | click to set where the robot appears in play | +| object | `0` | stamp a kit archetype (`[` `]` picks from 60+) or a blank object; click again to select | + +Brush/eraser size: **`,`** smaller, **`.`** bigger. The **fill** bucket (`4`) is your fast filler — outline a shape with lines, then click inside it. + +**Sealed areas:** fill only stays inside lines that fully enclose a space. If a +fill suddenly floods the whole screen, your outline has a gap — undo and close it. +The status line warns you (`LEAK?`). + +## Keys + +- `Tab` edit ⇄ play · `Esc` quit +- `1`–`0` tools · `L` cycle view +- `[` `]` choose sprite · `,` `.` brush size · `R` reload sprites +- `-` `=` previous / next room (the top strip has `<` `>` too) +- `Z` or `Backspace` undo · `C` clear room · `K` save · `O` load +- In play: **arrows / click** to walk · **type a command + Enter** (try `talk `) · in a conversation press **1–6** to choose, **Esc** to leave · `F1` CRT · `F2` sound + +## Saving + +`K` saves to `rooms/room.json` (the editable list of your strokes, not flat pixels — +so it stays re-openable and tiny). The game **auto-loads** that file on startup, and +`O` reloads it. The cyan circle on the canvas is where the robot spawns in play. + +--- + +# Bring your own pixel art (Aseprite / Inkscape) + +The dream loop: **draw a thing → drop the PNG in `sprites/` → press `R` → place it with the image tool (`7`).** + +## The spec (what a sprite PNG should be) + +- **Format:** PNG **with transparency** (alpha). Transparent pixels stay see-through in game; a sprite with no transparency acts like a solid tile. +- **Size = game size.** The room is 160×168 "fat" pixels — draw small, at 1:1. Rough guide: a person ≈ 12–20 px tall, a crate ≈ 24 px, a door ≈ 30×50. A 500 px sprite will fill the whole screen. +- **Colours:** ideally the MRPGI EGA 16 — load `palettes/mrpgi-ega.gpl` into your tool. Off-palette art still works; it's auto-snapped to the nearest EGA colour on load. +- **Name = id.** Lowercase, no spaces: `crate.png`, `barrel.png`, `becky.png`. The filename (minus `.png`) is what shows in the sprite tray. +- **Where:** the `sprites/` folder. (Starter `crate` / `barrel` / `sign` are already there — replace them or add your own.) + +## Aseprite (pixel art) + +1. **Load the palette:** Palette panel → ☰ menu → *Load Palette* → `palettes/mrpgi-ega.gpl`. Now you're painting in the exact game colours. (Bonus: *Sprite → Color Mode → Indexed* = pixel-perfect, zero quantization.) +2. New sprite, small (e.g. 24×24), transparent background. +3. Draw. Leave everything that isn't the object transparent. +4. **Export:** File → *Export Sprite Sheet* or just *Save As* a `.png` into `sprites/`. +5. In MRPGI: press **`R`**, tool **`7`**, **`[` `]`** to your sprite, **click** to place. + +## Inkscape (vector → pixel) + +1. Draw your art. *File → Document Properties* → set a small px canvas (e.g. 24×24). +2. **Export:** *File → Export* → Page tab, set width/height in **px**, transparent background → export PNG into `sprites/`. +3. Same place step in MRPGI (`R`, `7`, place). + +## How a placed sprite behaves + +The image tool **stamps** the sprite into the room picture and applies your current **meaning** tag underneath it. So: +- want a solid crate? pick the **wall** tag *before* you stamp → you can't walk through it. +- want a walk-over rug? pick **floor** (or **none** to leave walkability alone). + +It's baked into the room and saved in `room.json`. (A *live* object — a movable entity with a name + verbs for items/NPCs — is the next slice; just ask.) + +## One quirk: fat pixels + +Game pixels are twice as wide as tall (classic AGI). A perfect circle in Aseprite shows up slightly wide in game. Ignore it early — it's part of the retro look. + +--- + +# Build a world (multiple rooms) + +One room is a screen. A *world* is rooms linked by their edges. + +- The **top strip** shows `room N` with `<` `>` to move between rooms (also keys `-` / `=`). Going `>` past the last room makes a fresh blank one. Switching rooms **auto-saves** the one you're leaving. +- Each room saves to its own file: `rooms/room0.json`, `rooms/room1.json`, … +- **Exits** (top strip): the `N E S W` buttons. Click one to cycle its target room number (`N:-` → `N:0` → `N:1` … → off). So `E:1` means "walk off the right edge → arrive in room 1". +- **Set the entrance** with the **spawn** tool (`9`): click where the robot should appear. The cyan circle shows it. + +## Make two linked rooms + +1. In **room 0**, leave a walkable gap at the **right** edge (don't wall it). Set exit **`E:1`**. +2. Press `>` to go to **room 1**, draw it, leave a walkable gap on its **left** edge, set exit **`W:0`**. +3. Press `<` back to room 0, press **`Tab`** to play, walk off the right edge → you're in room 1. Walk back left → room 0. You built a world. 🌍 + +Tip: if the robot seems stuck after a transition, the edge it arrived at was walled — leave a floor gap there (it otherwise falls back to that room's spawn point). + +--- + +# Objects you can talk to (the parser) + +This is where it becomes a *game*. The **object** tool (`0`) places a sprite as a real thing — and in play you can **type at it.** + +## Place an object +1. Tool **`0`** (object). Press **`[` `]`** to flip through the **kit** — 60+ ready-made archetypes (orc, console, cocktail, coffin, sword, spellbook…), shown in the panel as `kit [ ]: `. **Click** the canvas to stamp the selected one — fully written (look/use/needs/wins/synonyms all filled in). Cycle to `(blank)` to instead drop a bare object named after the current sprite. +2. Click an object to **select** it. The inspector has editable fields — **name**, **look text**, **use text**, **needs item**, **synonyms** — plus **takeable / wins** toggles and **delete**. +3. To write its description: click the **look text** field, type your line (a caret shows), press **Enter** to jump to the next field. Click the canvas or press **Esc** to finish typing. +4. `K` saves it with the room. + +## Talk to it (in play) +Press **`Tab`** to play. There's a **`>` command line** at the bottom — type and hit **Enter**: +- `look barrel` → its description +- `take key` → into your inventory (if it's takeable) +- `inventory` (or `i`) → what you're carrying +- `look around` → everything in the room +- `help` → the verb list + +Arrows still walk while you type. The parser is forgiving now: it ignores filler words ("the", "a", "at") and knows verb synonyms — `look` = `examine` = `x` = `check` = `inspect`; `take` = `get` = `grab` = `pick`. And the **synonyms** field lets an object answer to other nouns: put `box wood cargo` on a crate and `examine the box` just works. + +## Try it +1. Edit: tool `0`, drop a `crate` and a `barrel`, press `K`. +2. `Tab` to play → type `look around`, then `look crate`, then `take barrel`, then `inventory`. The game answers you. 🤖💬 + +--- + +# Make a puzzle (locks, keys, winning) + +Objects can now *do* things — no code, just fields. Each object's inspector has: +- **use text** — what `use ` / `open ` prints. +- **needs item** — an item it requires. Empty = no lock. If set, `use` only works when that item is in your inventory; otherwise it says *"It's locked. You need: key."* +- **wins** toggle — using it successfully **wins the game** (a YOU WIN banner). + +## A complete 30-second game +1. Drop a sprite, select it, set **name** = `key`, **takeable** = on. (That's your key.) +2. Drop another, set **name** = `gate`, **use text** = *"The gate groans open. You're free!"*, **needs item** = `key`, **wins** = yes. +3. `K` to save, `Tab` to play. +4. Type `use gate` → *"It's locked. You need: key."* +5. `take key`, then `use gate` → your line prints + **★ YOU WIN ★** 🏆 + +`use` also accepts `open`, `unlock`, `push`, `pull`, `turn`. Keys, locks, and a win condition — a real puzzle, authored entirely with fields. + +--- + +# Natural language (the local AI lane) + +When the parser doesn't recognise your verb, it quietly asks a **local LLM** to translate your sentence into one *real* command — then runs THAT through the normal game logic. The AI bends the *language*; your game still decides what's *possible*. It can't invent items or outcomes. It runs on a background thread (the game never freezes) and shows what it decided, e.g. `(≈ open crate)`. + +## It's already set up on your Mac +Ollama is running with your Gemma models. Because your models have custom names, point the engine at one when you launch: + +```sh +./run.sh +# (which is just: MRPGI_AI_MODEL="hf.co/OBLITERATUS/gemma-4-E4B-it-OBLITERATED:latest" cargo run ) +``` + +Then in play, with a `gate` / `key` / `crate` around, try: +- `pry the lid off the box` → *(≈ open crate)* +- `yank the gate open` → *(≈ use gate)* +- `grab that shiny thing` → *(≈ take key)* + +The bottom bar shows `ai: ollama/` so you know it's wired. + +## Settings (env vars) +| Var | Default | Meaning | +|-----|---------|---------| +| `MRPGI_AI` | `ollama` | `ollama` · `openrouter` · `off` | +| `MRPGI_AI_MODEL` | `gemma3:4b` | model tag — set to your Ollama tag | +| `MRPGI_AI_URL` | `http://localhost:11434/api/generate` | endpoint override | +| `OPENROUTER_API_KEY` | — | required when `MRPGI_AI=openrouter` | + +**OpenRouter instead of local:** +```sh +MRPGI_AI=openrouter OPENROUTER_API_KEY=sk-... MRPGI_AI_MODEL=google/gemma-2-9b-it:free cargo run +``` + +If Ollama isn't running or the model's missing, the AI lane just stays quiet and you get the normal "I don't understand" — nothing breaks. + +--- + +# Talk & tunes (NPCs + ambient music) + +## Talk to NPCs +An object can carry a **branching dialogue tree**. In play, `talk ` (or `speak` / `ask` / `chat` / `greet`) starts a conversation: the NPC speaks, and you pick a reply by pressing its **number key** (`1`–`6`). Choices branch to other lines; some end the chat. **Esc** leaves early. Walking pauses while you talk. + +Try it: in the fantasy world, head **south** from the forest to the inn and type `talk barkeep` — there are a few branches to explore. + +Authoring is JSON for now: a `dialogue` array on the object (see the inn villager in `rooms/room3.json`). Each node has `says` + `choices`; each choice has `text` + `goto` (a node index, or `-1` to end). A visual tree-builder is a future step. + +## Ambient music +Each room can loop a **mood**. In the editor, the top strip has a **♪ button** — click it to cycle: off · calm · eerie · tense · jolly · spooky. It saves with the room (`K`) and plays in play mode, switching as you move between rooms. **F2** mutes music + SFX together. diff --git a/MANUAL.md b/MANUAL.md new file mode 100644 index 0000000..b7d014a --- /dev/null +++ b/MANUAL.md @@ -0,0 +1,515 @@ +# MRPGI — The Complete Manual + +**Monster Robot Party Game Interpreter** — a new-school, Apple-Silicon-native +reimagining of Sierra's 1980s AGI adventure engine, written in Rust + macroquad. +You paint rooms, drop in objects, write puzzles, and walk through them — with a +text parser, optional local-AI understanding, branching dialogue, chiptune sound, +and ambient music. No external level files, no build step beyond `cargo`. + +This manual is exhaustive. Jump around with the table of contents. + +--- + +## Table of contents +1. [What MRPGI is](#1-what-mrpgi-is) +2. [Install & requirements](#2-install--requirements) +3. [Running it](#3-running-it) +4. [The two modes](#4-the-two-modes) +5. [Core concepts (read this)](#5-core-concepts-read-this) +6. [The editor — full reference](#6-the-editor--full-reference) +7. [Keyboard cheat sheet](#7-keyboard-cheat-sheet) +8. [Building a world (rooms & exits)](#8-building-a-world-rooms--exits) +9. [Objects & game logic](#9-objects--game-logic) +10. [The parser](#10-the-parser) +11. [The AI parser lane](#11-the-ai-parser-lane) +12. [Dialogue trees](#12-dialogue-trees) +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) + +--- + +## 1. What MRPGI is + +AGI was the engine behind *King's Quest*, *Space Quest*, and *Police Quest*. Its +genius was a **dual-buffer** model: every room is drawn into a *visual* screen +(what you see) **and** a hidden *priority* screen (depth + walkability). MRPGI +keeps that soul and modernizes everything around it: + +- **In-engine room editor** — paint walls, floors, and walkability by hand. +- **Sprite pipeline** — drop PNGs in a folder; they're auto-quantized to EGA. +- **Multi-room worlds** with edge exits. +- **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**. +- **A content kit**: 61 click-to-place archetypes across fantasy, sci-fi, + monsters, "saucy" nightlife, and classic horror. + +Everything saves to small, human-readable JSON. The whole thing is one native +binary built with `cargo`. + +--- + +## 2. Install & requirements + +**Required:** +- **Rust** (stable, 1.85+). Install from or Homebrew. +- That's it for building — macroquad pulls its own dependencies and renders + natively (Metal on macOS). No system libraries to install. + +**Optional (for the AI parser):** +- **[Ollama](https://ollama.com)** running locally with a small instruct model + (e.g. a Gemma 4B). Free, offline, private. Or an **OpenRouter** API key. + +First build downloads the crate tree and takes a minute or two; subsequent builds +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 +``` + +### 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`). | + +```sh +cargo run -- --kit # install + play the fantasy starter world +cargo run -- --sample # install + play the robot escape demo +``` + +### AI environment variables +| Var | Default | Meaning | +|-----|---------|---------| +| `MRPGI_AI` | `ollama` | `ollama` · `openrouter` · `off` | +| `MRPGI_AI_MODEL` | `gemma3:4b` | the model tag (set to YOUR Ollama tag) | +| `MRPGI_AI_URL` | `http://localhost:11434/api/generate` | endpoint override | +| `OPENROUTER_API_KEY` | — | required when `MRPGI_AI=openrouter` | + +```sh +MRPGI_AI_MODEL="your-gemma-tag" cargo run +MRPGI_AI=openrouter OPENROUTER_API_KEY=sk-... MRPGI_AI_MODEL=google/gemma-2-9b-it:free cargo run +MRPGI_AI=off cargo run # deterministic parser only +``` + +--- + +## 4. The two modes + +MRPGI is always in one of two modes; **`Tab`** flips between them: + +- **EDIT** — the painting/authoring mode. Left panel of tools, a canvas, a top + strip for world/room controls. The engine boots here. +- **PLAY** — you become the robot. Walk with arrows or click, type commands, talk + to NPCs. A bottom panel shows the transcript + command line. + +`Esc` quits (in edit it first un-focuses a text field; in play it first ends a +conversation). + +--- + +## 5. Core concepts (read this) + +### The dual buffer — LOOK vs MEANING +Every pixel of a room has **two** independent properties: + +- **LOOK** — its colour (one of 16 EGA colours). What the player sees. +- **MEANING** — what it *is* to the engine: walkable floor, a wall, water, a + trigger, or a depth band. Stored in a hidden "priority" buffer. + +**Colour does not imply meaning.** A brown floor and a brown wall look identical +but behave differently. When you paint, you set a colour *and* (optionally) a +meaning tag; they're applied to the same region but stored separately. This is the +single most important idea in MRPGI. + +### Meaning tags +| Tag | Effect on the priority buffer | +|-----|-------------------------------| +| **floor** | walkable, with automatic depth (lower on screen = drawn in front) | +| **wall** | blocked — the robot can't enter | +| **water** | reserved (special) | +| **trigger** | reserved (signal line for future logic) | +| **none** | paint colour only; leave walkability unchanged | + +By default an empty room is **entirely walkable** — you paint *walls* where you +don't want walking. + +### The picture +The play area is **160 × 168 logical pixels**, displayed at a **2:1 pixel ratio** +(each pixel is twice as wide as tall — classic AGI). Colours are the 16-colour +EGA palette (indices 0–15). A perfect circle drawn in art tools looks slightly +wide in-game; that's intentional retro flavour. + +### EGA palette +`0` black · `1` blue · `2` green · `3` cyan · `4` red · `5` magenta · `6` brown · +`7` light gray · `8` dark gray · `9` light blue · `10` light green · +`11` light cyan · `12` light red · `13` light magenta · `14` yellow · `15` white. + +### The ego +"Ego" is the AGI term for the player avatar (the little robot). Its position is +its **feet** (bottom-centre of its sprite), which is what determines its depth and +collision. In play it's blocked by `wall` pixels and walks off `exit` edges. + +--- + +## 6. The editor — full reference + +The editor screen: a **tool panel** on the left, the **canvas** on the right, and +a **world strip** across the top of the canvas. + +### Tools (number keys, or click in the panel) +| Key | Tool | What it does | +|-----|------|--------------| +| `1` | **brush** | freehand paint with the current colour + meaning (size via `,` `.`) | +| `2` | **line** | click to add points; **right-click or Enter** finishes the polyline | +| `3` | **rect** | drag a filled rectangle | +| `4` | **fill** | flood-fill a sealed region (the bucket) — fast | +| `5` | **pick** | eyedropper; grab a colour off the canvas | +| `6` | **erase** | rub back to blank + walkable | +| `7` | **image** | stamp a sprite from `sprites/` (`[` `]` to choose, `R` to reload) | +| `8` | **oval** | drag a filled ellipse | +| `9` | **spawn** | click to set where the robot appears in play (cyan circle) | +| `0` | **object** | place a live object — see below | + +### Colour & meaning +- **Palette** (left panel): click a swatch to set the current LOOK colour. +- **Meaning** buttons: floor / wall / water / trigger / none — the MEANING applied + by fills, rects, brush, etc. + +### View modes (`L` cycles) +- **look** — the plain picture. +- **both** — picture + walls tinted red, water blue, triggers green (so you can + *see* what's walkable). +- **ctrl** — the raw meaning map (depth + control). + +### The object tool & kit browser (`0`) +With the object tool active the panel shows **`kit [ ]: `**. Press `[` `]` +to flip through 60+ ready-made archetypes (orc, console, cocktail, coffin…). Click +an empty spot to **stamp** the selected archetype, fully written. Cycle to +`(blank)` to drop a bare object named after the current sprite. Click an existing +object to **select** it; the inspector then shows editable fields: + +- **name** — what the parser calls it. +- **look text** — response to `look`. +- **use text** — response to `use`/`open`. +- **needs item** — an item required to `use` it (empty = unlocked). +- **synonyms** — extra nouns it answers to (space-separated). +- **takeable / fixed** toggle, **wins** toggle, **delete**. + +Click a field, type, `Enter` jumps to the next field, `Esc` finishes. + +### The world strip (top) +- **room N** + `<` `>` — move between rooms (also `-` `=`). Switching auto-saves. +- **exits** `N E S W` — click to cycle each edge's target room number. +- **♪** — cycle the room's ambient music mood (off · calm · eerie · tense · jolly · spooky). + +### Saving +- `K` saves the current room to `rooms/room.json` (an editable list of strokes + + objects + exits + spawn + music). `O` reloads it. `C` clears, `Z`/`Backspace` + undoes. The game auto-loads `rooms/room0.json` on startup. + +--- + +## 7. Keyboard cheat sheet + +**Global:** `Tab` edit ⇄ play · `Esc` quit (or un-focus / end conversation) · +`F1` CRT scanlines · `F2` mute sound + music. + +**Editor:** `1`–`0` tools · `L` view · `[` `]` choose sprite / kit archetype · +`,` `.` brush size · `-` `=` prev / next room · `Z`/`Backspace` undo · `C` clear · +`K` save · `O` load · `R` reload sprites + kit. + +**Play:** arrows / click to walk · type a command + `Enter` · `1`–`6` choose a +dialogue reply · `Esc` leave a conversation. + +--- + +## 8. Building a world (rooms & exits) + +A room is one screen. A world is rooms linked by their edges. + +1. Paint a room. Leave a **walkable gap** in a wall where you want a doorway. +2. On the strip, set that edge's **exit** to a room number (e.g. `E: 1`). +3. Press `>` to go to that room (going past the last room makes a new blank one), + paint it, and set a matching exit back. +4. In play, **walk off the edge with an exit** → you arrive in the target room, + entering from the opposite side. + +Rooms are files: `rooms/room0.json`, `room1.json`, … Room 0 is where the game +starts. The **spawn** tool sets each room's entry point (used if the edge you +arrive at is walled). + +--- + +## 9. Objects & game logic + +Objects are sprites you can interact with. Each carries: + +- a **name** + **synonyms** (what the parser matches), +- **look text** (shown on `look`), +- **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. + +This is enough for the classic adventure loop **with no code**: + +> Put a `key` (takeable) in one room and a `gate` in another with **needs = key**, +> **use text** = *"The gate grinds open…"*, **wins = on**. Now: `take key` → +> travel → `use gate` → **win**. + +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). + +--- + +## 10. The parser + +Type a sentence; it's lower-cased, filler words are dropped, the first word maps to +a canonical verb, and the rest is the noun (matched against object names + +synonyms, loosely). + +**Verbs (and their synonyms):** +- **look** — look, l, examine, x, inspect, check, read, view, see, study +- **take** — take, get, grab, pick, snatch, steal, pocket, collect, nab +- **use** — use, open, unlock, activate, push, pull, turn, operate +- **drop** — drop, discard, leave, put +- **talk** — talk, speak, ask, chat, greet, say +- **inventory** — inventory, inv, i, items, bag +- **help** — help, ?, commands, verbs + +**Ignored filler:** the, a, an, at, to, with, on, of, my, your, some, please, go, up. + +`look` with no noun (or `look around`) lists what's in the room. Unknown verbs fall +through to the AI lane (if enabled) or print "I don't understand". + +--- + +## 11. The AI parser lane + +When the deterministic parser can't recognise your verb, MRPGI can hand the raw +sentence to a small LLM, which translates it into **one allowed command** — then +re-runs *that* through the normal logic. The AI bends the *language*; your game +still decides what's *possible*. It never invents items or outcomes, and it runs on +a background thread so the game never stalls. The chosen command is shown as +`(≈ open crate)`. + +**Setup (local, recommended):** run Ollama with an instruct model and point MRPGI +at it via `MRPGI_AI_MODEL` (see §3). Default provider is `ollama`. If Ollama isn't +running or the model is missing, the lane stays silent and you get the normal +deterministic behaviour — nothing breaks. + +**OpenRouter:** set `MRPGI_AI=openrouter`, `OPENROUTER_API_KEY`, and a +`MRPGI_AI_MODEL` like `google/gemma-2-9b-it:free`. + +The bottom bar shows `ai: ollama/` (or `off`). + +--- + +## 12. Dialogue trees + +An object can carry a **branching conversation**. In play, `talk ` starts it: +the NPC speaks and you press a **number key (1–6)** to choose a reply. Choices jump +to other lines; some end the chat. `Esc` leaves. Walking pauses while you talk. + +Dialogue is authored as a `dialogue` array on the object (a visual builder is +planned). Each entry is a **node**; node 0 is the entry. See the inn barkeep in +`rooms/room3.json` for a working example: + +```json +"dialogue": [ + { "says": "You're for the cave, aren't you?", + "choices": [ + { "text": "What's down there?", "goto": 1 }, + { "text": "Just passing through.", "goto": 3 } + ] }, + { "says": "Orcs. Dark. A spellbook nobody should read aloud.", + "choices": [ { "text": "Thanks.", "goto": -1 } ] } +] +``` + +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). + +--- + +## 13. Sound & music + +All audio is **synthesized at runtime** (square waves → PCM, no files), in the +spirit of AGI's PCjr beeper. + +**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 +4-note fanfare on **win**. + +**Ambient music:** each room can loop one of five moods (calm / eerie / tense / +jolly / spooky), set with the **♪** strip button in the editor and saved per room. +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. + +--- + +## 14. The art / sprite pipeline + +Sprites live in `sprites/` as PNGs. On load, any image is **quantized to the EGA +palette** (with transparency from its alpha), so it depth-sorts and walks-behind +like native art. Replace any sprite and press **`R`** to reload. + +**The sprite spec:** +- **PNG with transparency.** Transparent pixels are see-through. +- **Small, at game scale.** A prop is ~12–28 px tall; draw 1:1. +- **Filename = id.** lowercase, no spaces (`orc.png`). The object's `sprite` field + references this name. A missing sprite shows as a magenta placeholder. +- **Bottom-centre = feet.** Stand the thing on the bottom edge of the canvas. +- **EGA colours** ideal but not required — load `palettes/mrpgi-ega.gpl` into + Aseprite/Inkscape, or let the engine snap your colours on load. + +**Aseprite:** Palette → Load Palette → `palettes/mrpgi-ega.gpl`; draw small; +export PNG to `sprites/`; press `R`. + +**Inkscape:** set a tiny px page; Export PNG (transparent) to `sprites/`. + +**AI (Gemini "Nano Banana" / Imagen):** each theme's `gemini_sprites.md` has a +shared STYLE line + per-sprite prompts. Generate on a solid magenta background, key +it out to transparency, save into `sprites/`. (See also `palettes/mrpgi-ega.gpl` +and `palettes/mrpgi-ega.hex`.) + +--- + +## 15. The kit & theme packs + +`cargo run -- --kit` generates the **fantasy starter kit** and installs a playable +4-room world (forest → cave → dungeon → inn, with a key/spellbook/altar quest). + +The reusable library lives in `kit/`: +- **`kit/objects.json`** — 13 fantasy object archetypes. +- **`kit/locations/`** — prefab rooms (forest, cave, dungeon, inn). +- **`kit/ART_MANIFESTO.md`** — exact sprite specs to replace the placeholders. +- **`kit/themes/{scifi,monsters,saucy,horror}/`** — each with `objects.json` + (12 archetypes), a sample `locations/*.json`, and `gemini_sprites.md`. + +**Use an object:** the kit browser (object tool, `[` `]`) stamps any of these by +clicking. Or copy a `{ … }` block from any `objects.json` into a room's `objects` +array. **Use a location:** `cp kit/themes/horror/locations/crypt.json rooms/room5.json`, +then wire its exits in the editor. + +--- + +## 16. Room file format (full schema) + +A room is a `RoomDoc` serialized as JSON: + +```json +{ + "strokes": [ , ... ], + "spawn": [x, y], + "exits": [N, E, S, W], // each: a room number or null + "objects": [ , ... ], + "music": 0 // 0=silence, 1..5 = mood +} +``` + +**Stroke** is an externally-tagged enum — exactly one key: +```json +{ "Rect": { "x":0,"y":0,"w":160,"h":168,"color":2,"meaning":"Floor" } } +{ "Line": { "pts":[[x,y],...], "color":15 } } +{ "Brush": { "pts":[[x,y],...], "color":6,"meaning":"Wall","size":2 } } +{ "Ellipse": { "x":..,"y":..,"w":..,"h":..,"color":..,"meaning":"None" } } +{ "Fill": { "x":..,"y":..,"color":..,"meaning":"Floor" } } +{ "Image": { "name":"crate","x":..,"y":..,"meaning":"Wall" } } +``` +`meaning` is one of `"None"`, `"Floor"`, `"Wall"`, `"Water"`, `"Trigger"`. +`color` is an EGA index 0–15. + +**ObjDef:** +```json +{ + "name": "gate", "sprite": "door", "x": 80, "y": 120, + "look": "A heavy gate.", "takeable": false, + "synonyms": "door portcullis", "use_text": "It grinds open!", + "needs": "key", "wins": true, + "dialogue": [ , ... ] +} +``` +**DlgNode:** `{ "says": "...", "choices": [ { "text": "...", "goto": 1 } ] }` — +`goto` is a node index or `-1` to end. + +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 +and re-editable. + +--- + +## 17. Project layout & architecture + +``` +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) +``` + +**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. + +--- + +## 18. 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. +- **A fill flooded the whole screen** → your outline has a gap; the region wasn't + sealed. `Z` to undo and close the gap. +- **Robot stuck after a room transition** → the edge you arrived at is walled; + leave a walkable gap there (it otherwise falls back to the room's spawn). +- **AI does nothing** → check the bottom bar `ai:` readout; ensure Ollama is + running and `MRPGI_AI_MODEL` matches a pulled model (`ollama list`). It only + fires on *unknown verbs*. +- **No sound** → some machines lack an audio backend; MRPGI stays silent rather + than crashing. Check `F2` isn't muted. +- **Typing a letter flipped a mode** → in play, letters go to the command line; + use `Tab`/`F1`/`F2`/`Esc` (non-letters) for controls. + +--- + +## 19. Roadmap & known gaps + +- In-editor **dialogue-tree builder** (dialogue is JSON-authored for now). +- 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. + +--- + +*MRPGI — built from "let's reimagine AGI" into a complete, AI-assisted, audible +adventure-game maker. Have fun making weird little worlds.* 🤖🎸 diff --git a/README.md b/README.md new file mode 100644 index 0000000..22d6df2 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# MRPGI — Monster Robot Party Game Interpreter + +A **new-school, Apple-Silicon-native reimagining of Sierra's 1980s AGI engine** — +the tech behind *King's Quest*, *Space Quest*, and *Police Quest* — rebuilt in +Rust + [macroquad](https://macroquad.rs) so you can make cool, fun adventure games. + +It keeps the *soul* of AGI (vector rooms, the invisible priority/depth screen, +loops-and-cels sprites, a verb-noun parser, flags & vars, the ego) and bridges +it forward (Retina-crisp rendering, hand-editable text resources, a real +embedded scripting language, and an optional AI parser). + +## Run it + +```sh +cargo run # dev build (fast to recompile) +cargo run --release # smooth, optimized build +``` + +A window opens with the demo room. **Walk the robot with the arrow keys or by +clicking** — and stroll *behind the column* to watch the priority system place +you correctly in depth. `N` toggles CRT scanlines. `Esc` quits. + +## How it works (the AGI model, faithfully) + +Every room is drawn into **two buffers at once**: + +| Buffer | What it holds | Why | +|--------|---------------|-----| +| **visual** | palette indices (0–15, EGA) | what you see | +| **priority** | depth bands (4–15) + control lines (0–3) | depth sorting & collision | + +Lower on screen ⇒ higher priority ⇒ drawn nearer the camera. The ego is drawn a +pixel at a time, *only where its priority ≥ the scenery's* — that's the whole +"walk behind the tree" trick, with zero per-object Z bookkeeping. Control lines +(priority `0`) are walls the ego can't cross. + +## Source map + +| File | Role | +|------|------| +| `palette.rs` | the 16-color EGA palette (indices → RGBA) | +| `framebuffer.rs` | the visual + priority buffers and the `band(y)` depth table | +| `picture.rs` | vector room painting (`PicOp` rectangles / lines / pixels) | +| `view.rs` | sprites as loops + cels, authored from tiny ASCII grids | +| `sprite.rs` | the ego: movement, click-to-walk, collision, walk animation | +| `room.rs` | the hand-authored demo room | +| `main.rs` | window, game loop, fixed-step cycles, compositing, HUD | + +## Asset pipeline (the new-school bit) + +Images don't sit *on top* of the game — they're folded *into* it. Any PNG is +quantized to the EGA palette (optional Bayer dithering for smooth retro +gradients) and becomes indexed pixels, so from that moment it depth-sorts, gets +occluded, and walks-behind exactly like hand-authored art. Three ways in: + +- **Tile** an image across a region (textures — e.g. the floor) +- **Pour** an image into a shape via `stamp_cel_fit` with a fit mode + (stretch / tile / center) — *the shape also stamps depth & collision.* + This is the "draw a shape, load an image into it" idea, realized. +- **Objects** — a PNG becomes a `Prop` that sorts with the ego + +Drop your own art into `assets/`, then press **R** in-game to hot-reload it. + +## Roadmap + +- [x] **v0.1 — rendering core**: vector room, priority depth, walkable ego, click-to-walk, CRT toggle +- [x] **v0.2 — asset pipeline**: PNG → EGA quantize + dither, tile / pour-into-shape / object insertion, hot-reload (`R`) +- [x] **v0.3 — room editor**: in-engine paint tool with edit/play modes, dual-layer LOOK+MEANING painting, flood-fill, tools (brush/line/rect/fill/pick/erase), undo, and JSON room save/load — see [EDITOR_GUIDE.md](EDITOR_GUIDE.md) +- [ ] **Priority-mask backgrounds**: a full-room art PNG + a companion mask PNG whose colors encode depth bands & control lines (the pro art workflow) +- [ ] **True-color "HD" layer mode**: skip quantization for painted backdrops; the priority buffer stays indexed +- [ ] **Polygon shapes** for `stamp_cel_fit` (not just rectangles) +- [ ] **Directional views** (4 loops + horizontal mirroring) for the robot +- [ ] **Text parser**: vocabulary with synonyms → `said("open","door")` matching, plus an on-screen input line +- [ ] **Rhai room logic**: rooms scripted in an AGI-flavored embedded language (`if said(...) { print(...) }`), hot-reloadable +- [ ] **Room transitions** (`new_room`), inventory, flags & vars +- [ ] **AI parser adapter**: when the classic parse misses, an LLM maps free text onto an *already-allowed* room command (deterministic core stays authoritative; works offline without it) +- [ ] **3-voice + noise chiptune synth** +- [ ] **Loadable game folders** (text/JSON resources) so games ship without recompiling the engine diff --git a/assets/crate.png b/assets/crate.png new file mode 100644 index 0000000..d84c9ab Binary files /dev/null and b/assets/crate.png differ diff --git a/assets/floor.png b/assets/floor.png new file mode 100644 index 0000000..a6278d0 Binary files /dev/null and b/assets/floor.png differ diff --git a/assets/poster.png b/assets/poster.png new file mode 100644 index 0000000..4a12c33 Binary files /dev/null and b/assets/poster.png differ diff --git a/kit/ART_MANIFESTO.md b/kit/ART_MANIFESTO.md new file mode 100644 index 0000000..9d6137e --- /dev/null +++ b/kit/ART_MANIFESTO.md @@ -0,0 +1,53 @@ +# MRPGI starter-kit art manifesto + +The kit ships **crude placeholder sprites** so everything's visible immediately. +Replace any of them with real art and the game upgrades instantly — **same +filename in `sprites/`, then press `R` in the editor.** This doc is the spec for +making the real ones. + +## The sprite spec (always) +- **PNG with transparency.** Transparent pixels = see-through in game. +- **Small, at game scale.** The play area is 160×168 *fat* pixels. A prop is ~12–28 px; draw 1:1. +- **EGA 16 colours.** Load `palettes/mrpgi-ega.gpl` into Aseprite/Inkscape. Off-palette art is auto-snapped to nearest EGA on load (so it still works, just less crisp). +- **Filename = id.** lowercase, no spaces: `orc.png`, `spellbook.png`. The name is what the game and objects reference. +- **Bottom-centre is the "feet."** The sprite is drawn standing on the click point, so put the base of the thing at the bottom of the canvas. +- **Fat pixels:** game pixels are 2× wide. A circle drawn round looks slightly wide in game (classic AGI). Ignore it, or pre-squash horizontally if you care. + +## The kit sprites (replace these) + +| file | size (w×h) | what | key EGA colours | +|------|-----------|------|-----------------| +| `tree` | 18×26 | leafy tree, trunk at bottom | green 2/10, brown 6 | +| `rock` | 16×12 | mossy boulder | gray 7/8 | +| `orc` | 14×22 | green brute, red eyes | green 2/10, red 12 | +| `sword` | 10×26 | upright blade + hilt | light gray 7, brown 6, yellow 14 | +| `chest` | 20×15 | banded wooden chest | brown 6, dark gray 8, yellow 14 | +| `torch` | 8×24 | stick + flame | brown 6, red 12, yellow 14 | +| `potion` | 10×18 | vial, red liquid, cork | cyan 11 glass, magenta 13, brown 6 | +| `door` | 16×26 | banded wooden door + knob | brown 6, dark brown, yellow 14 | +| `villager` | 12×22 | robed person, skin head | blue 1/9, skin (light red 12) | +| `spellbook` | 16×12 | purple tome + rune | magenta 5/13, white 15, yellow 14 | +| `key` | 16×8 | iron key, bow + teeth | yellow 14 | +| `altar` | 20×16 | stone block + glow | gray 7/8, cyan 11 | +| `crate` `barrel` `sign` | — | already generated; same idea | brown/gray | + +(Want more? Same recipe: `skeleton`, `goblin`, `coin`/`gold`, `shield`, `bottle`, +`mushroom`, `lantern`, `gravestone`, `bookshelf`, `bed`, `well`, `bridge`.) + +## How to make them + +**Aseprite (best):** +1. Palette ☰ → *Load Palette* → `palettes/mrpgi-ega.gpl`. (Optional: *Sprite → Color Mode → Indexed* = pixel-perfect.) +2. New sprite at the size above, transparent bg. Draw the thing standing on the bottom edge. +3. Export `.png` into `sprites/` with the matching name. `R` in the editor to reload. + +**Inkscape:** draw it, set a tiny px page (Document Properties), Export → PNG (transparent) into `sprites/`. + +**AI rough-start (Gemini/Imagen — then clean up in Aseprite):** +> `16-colour EGA pixel-art sprite of a {THING}, about {W}×{H} pixels, transparent background, single object centred and standing on the bottom edge, bold readable silhouette, flat colours, retro 1980s Sierra adventure style, NO text, NO drop shadow, NO background.` +> +> e.g. THING = "a green orc with red eyes holding a dagger", W×H = 14×22. + +Image models are sloppy at tiny exact pixel art, so treat their output as a base +and tidy it in Aseprite against the EGA palette. For *backgrounds* (whole rooms), +you can also just paint them with the editor's own tools — no files needed. diff --git a/kit/README.md b/kit/README.md new file mode 100644 index 0000000..9af58c3 --- /dev/null +++ b/kit/README.md @@ -0,0 +1,39 @@ +# MRPGI fantasy starter kit + +Drop-in building blocks for fantasy games. Regenerate any time with: + +```sh +cargo run -- --kit +``` + +That writes placeholder sprites into `sprites/`, this kit into `kit/`, **and** +installs a playable 4-room fantasy world into `rooms/` (your existing rooms are +backed up to `*.json.bak` first). + +## What's here +- **`objects.json`** — a catalog of object archetypes (sword, spellbook, orc, key, chest, door, altar, potion, torch, villager, tree, rock, sign). Each is a ready-made object with name / look text / use text / `needs` / `wins` / synonyms already written. +- **`locations/*.json`** — prefab rooms: `forest`, `cave`, `dungeon`, `inn`. Each is a complete, valid room (painted scenery + objects + spawn + exits). +- **`ART_MANIFESTO.md`** — exact specs to replace the crude placeholder sprites with real art. + +## How to drop a location into your game +Rooms live at `rooms/room0.json`, `room1.json`, … (room 0 is where you start). + +```sh +cp kit/locations/forest.json rooms/room4.json +``` +Then launch, press `>` in the top strip to walk to room 4, and edit away. (Re-wire +its **exits** on the top strip so it connects to your other rooms.) + +## How to drop an object into a room +Open `kit/objects.json`, copy one object block, and paste it into the `objects: +[ … ]` array of any `rooms/roomN.json` (adjust its `x`/`y`). Or just place a +sprite with the object tool (`0`) in the editor and copy the look/use/needs text +from the catalog into its fields. + +## The built-in fantasy quest (what `--kit` installs) +0. **Forest** — grab the **sword**. Paths: cave (east), inn (south). +1. **Cave** — an **orc**, a **chest** (needs a key), and the **spellbook** on a ledge. +2. **Dungeon** (north of cave) — an **altar**. `use altar` with the spellbook → **win**. +3. **Inn** (south of forest) — the **villager**, a **potion**, and the **key** (for the chest). + +Solution: forest → inn (get key) → forest → cave (get spellbook; key opens the chest for gold) → dungeon → `use altar`. Win. Try natural language too — `pry the chest open`, `read the spellbook`, `bonk the orc`. diff --git a/kit/locations/cave.json b/kit/locations/cave.json new file mode 100644 index 0000000..8f687e6 --- /dev/null +++ b/kit/locations/cave.json @@ -0,0 +1,164 @@ +{ + "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": 65, + "h": 16, + "color": 0, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 95, + "y": 0, + "w": 65, + "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": 70, + "color": 0, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 0, + "y": 100, + "w": 6, + "h": 68, + "color": 0, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 154, + "y": 0, + "w": 6, + "h": 168, + "color": 0, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 40, + "y": 60, + "w": 10, + "h": 6, + "color": 7, + "meaning": "None" + } + }, + { + "Rect": { + "x": 110, + "y": 80, + "w": 12, + "h": 6, + "color": 7, + "meaning": "None" + } + } + ], + "spawn": [ + 80, + 130 + ], + "exits": [ + 2, + null, + null, + 0 + ], + "objects": [ + { + "name": "orc", + "sprite": "orc", + "x": 92, + "y": 118, + "look": "A green orc picking its teeth with a dagger. It hasn't noticed you. Yet.", + "takeable": false, + "synonyms": "monster brute goblin", + "use_text": "You poke the orc. This was a mistake.", + "needs": "", + "wins": false + }, + { + "name": "spellbook", + "sprite": "spellbook", + "x": 122, + "y": 132, + "look": "A spellbook on a ledge, runes squirming. The orc is guarding it (badly).", + "takeable": true, + "synonyms": "book tome grimoire", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "torch", + "sprite": "torch", + "x": 36, + "y": 110, + "look": "A guttering wall-torch. Grab it — it's dark deeper in.", + "takeable": true, + "synonyms": "light flame", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "chest", + "sprite": "chest", + "x": 58, + "y": 140, + "look": "A banded chest, half-buried. Locked.", + "takeable": false, + "synonyms": "box trunk", + "use_text": "The key turns; old gold spills out across the cave floor.", + "needs": "key", + "wins": false + } + ] +} \ No newline at end of file diff --git a/kit/locations/dungeon.json b/kit/locations/dungeon.json new file mode 100644 index 0000000..a50ecbd --- /dev/null +++ b/kit/locations/dungeon.json @@ -0,0 +1,120 @@ +{ + "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": 58, + "y": 30, + "w": 44, + "h": 10, + "color": 13, + "meaning": "None" + } + } + ], + "spawn": [ + 80, + 138 + ], + "exits": [ + null, + null, + 1, + null + ], + "objects": [ + { + "name": "altar", + "sprite": "altar", + "x": 80, + "y": 98, + "look": "A stone altar humming with cold light. A book-shaped hollow waits in its surface.", + "takeable": false, + "synonyms": "shrine pedestal", + "use_text": "You lay the spellbook on the altar. Light pours up, a doorway tears open, and you step through. YOU ESCAPED — adventure complete. THE END.", + "needs": "spellbook", + "wins": true + }, + { + "name": "torch", + "sprite": "torch", + "x": 30, + "y": 120, + "look": "A torch in a rusted bracket.", + "takeable": true, + "synonyms": "light flame", + "use_text": "", + "needs": "", + "wins": false + } + ] +} \ No newline at end of file diff --git a/kit/locations/forest.json b/kit/locations/forest.json new file mode 100644 index 0000000..24e586c --- /dev/null +++ b/kit/locations/forest.json @@ -0,0 +1,164 @@ +{ + "strokes": [ + { + "Rect": { + "x": 0, + "y": 0, + "w": 160, + "h": 168, + "color": 2, + "meaning": "None" + } + }, + { + "Rect": { + "x": 6, + "y": 16, + "w": 148, + "h": 146, + "color": 10, + "meaning": "Floor" + } + }, + { + "Rect": { + "x": 0, + "y": 0, + "w": 160, + "h": 16, + "color": 6, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 0, + "y": 160, + "w": 65, + "h": 8, + "color": 6, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 95, + "y": 160, + "w": 65, + "h": 8, + "color": 6, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 0, + "y": 0, + "w": 6, + "h": 168, + "color": 6, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 154, + "y": 0, + "w": 6, + "h": 70, + "color": 6, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 154, + "y": 100, + "w": 6, + "h": 68, + "color": 6, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 30, + "y": 120, + "w": 18, + "h": 8, + "color": 2, + "meaning": "None" + } + }, + { + "Rect": { + "x": 100, + "y": 60, + "w": 22, + "h": 8, + "color": 2, + "meaning": "None" + } + } + ], + "spawn": [ + 24, + 130 + ], + "exits": [ + null, + 1, + 3, + null + ], + "objects": [ + { + "name": "tree", + "sprite": "tree", + "x": 44, + "y": 70, + "look": "An ancient tree, bark like old leather.", + "takeable": false, + "synonyms": "oak trunk wood", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "sword", + "sprite": "sword", + "x": 80, + "y": 132, + "look": "A notched iron sword someone left in the grass. Finders keepers.", + "takeable": true, + "synonyms": "blade weapon", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "rock", + "sprite": "rock", + "x": 120, + "y": 134, + "look": "A mossy boulder, smug and immovable.", + "takeable": false, + "synonyms": "boulder stone", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "sign", + "sprite": "sign", + "x": 80, + "y": 44, + "look": "A signpost: CAVE east, INN south.", + "takeable": false, + "synonyms": "notice post", + "use_text": "", + "needs": "", + "wins": false + } + ] +} \ No newline at end of file diff --git a/kit/locations/inn.json b/kit/locations/inn.json new file mode 100644 index 0000000..5a4e63f --- /dev/null +++ b/kit/locations/inn.json @@ -0,0 +1,154 @@ +{ + "strokes": [ + { + "Rect": { + "x": 0, + "y": 0, + "w": 160, + "h": 168, + "color": 6, + "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": 6, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 95, + "y": 0, + "w": 65, + "h": 16, + "color": 6, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 0, + "y": 160, + "w": 160, + "h": 8, + "color": 6, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 0, + "y": 0, + "w": 6, + "h": 168, + "color": 6, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 154, + "y": 0, + "w": 6, + "h": 168, + "color": 6, + "meaning": "Wall" + } + }, + { + "Rect": { + "x": 40, + "y": 122, + "w": 80, + "h": 28, + "color": 4, + "meaning": "None" + } + }, + { + "Rect": { + "x": 20, + "y": 24, + "w": 8, + "h": 8, + "color": 14, + "meaning": "None" + } + } + ], + "spawn": [ + 80, + 138 + ], + "exits": [ + 0, + null, + null, + null + ], + "objects": [ + { + "name": "villager", + "sprite": "villager", + "x": 60, + "y": 112, + "look": "A nervous barkeep. 'Heading to the cave? Take the key — and my prayers.'", + "takeable": false, + "synonyms": "barkeep person npc", + "use_text": "'Be careful in there,' the barkeep says, sliding you nothing useful.", + "needs": "", + "wins": false + }, + { + "name": "key", + "sprite": "key", + "x": 44, + "y": 140, + "look": "An iron key hanging by the door. The barkeep nods at you to take it.", + "takeable": true, + "synonyms": "ironkey", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "potion", + "sprite": "potion", + "x": 112, + "y": 132, + "look": "A complimentary vial of bubbling red liquid. Probably fine.", + "takeable": true, + "synonyms": "vial elixir flask", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "barrel", + "sprite": "barrel", + "x": 122, + "y": 120, + "look": "A barrel of inn ale. Smells like courage and mistakes.", + "takeable": false, + "synonyms": "ale keg", + "use_text": "", + "needs": "", + "wins": false + } + ] +} \ No newline at end of file diff --git a/kit/objects.json b/kit/objects.json new file mode 100644 index 0000000..9ed73d8 --- /dev/null +++ b/kit/objects.json @@ -0,0 +1,158 @@ +[ + { + "name": "sword", + "sprite": "sword", + "x": 80, + "y": 130, + "look": "A notched iron sword. It has Opinions about orcs.", + "takeable": true, + "synonyms": "blade weapon", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "spellbook", + "sprite": "spellbook", + "x": 80, + "y": 130, + "look": "A spellbook bound in something best not identified. The runes won't hold still.", + "takeable": true, + "synonyms": "book tome grimoire", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "key", + "sprite": "key", + "x": 80, + "y": 130, + "look": "A heavy iron key, cold and important-looking.", + "takeable": true, + "synonyms": "ironkey", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "potion", + "sprite": "potion", + "x": 80, + "y": 130, + "look": "A vial of bubbling red liquid. Almost certainly fine.", + "takeable": true, + "synonyms": "vial elixir flask", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "torch", + "sprite": "torch", + "x": 80, + "y": 130, + "look": "A guttering torch. It pushes the dark back a little.", + "takeable": true, + "synonyms": "light flame", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "orc", + "sprite": "orc", + "x": 80, + "y": 120, + "look": "A green orc, picking its teeth with a dagger. It hasn't noticed you. Yet.", + "takeable": false, + "synonyms": "monster brute goblin", + "use_text": "You poke the orc. It looks up. In hindsight, a mistake.", + "needs": "", + "wins": false + }, + { + "name": "villager", + "sprite": "villager", + "x": 80, + "y": 120, + "look": "A nervous villager. 'You're... not from the cave, are you?'", + "takeable": false, + "synonyms": "person man woman npc", + "use_text": "'Sorry, nothing for you,' the villager mumbles.", + "needs": "", + "wins": false + }, + { + "name": "chest", + "sprite": "chest", + "x": 80, + "y": 135, + "look": "A banded wooden chest. Locked.", + "takeable": false, + "synonyms": "box trunk", + "use_text": "The key turns; the lid yawns open and old gold spills out.", + "needs": "key", + "wins": false + }, + { + "name": "door", + "sprite": "door", + "x": 80, + "y": 120, + "look": "A stout wooden door, banded with iron.", + "takeable": false, + "synonyms": "gate", + "use_text": "The lock clunks and the door swings wide.", + "needs": "key", + "wins": false + }, + { + "name": "altar", + "sprite": "altar", + "x": 80, + "y": 100, + "look": "A stone altar humming with cold light. Something is meant to rest here.", + "takeable": false, + "synonyms": "shrine pedestal", + "use_text": "You lay the spellbook on the altar. A doorway tears open in the air. THE END.", + "needs": "spellbook", + "wins": true + }, + { + "name": "tree", + "sprite": "tree", + "x": 60, + "y": 80, + "look": "An ancient tree, bark like old leather.", + "takeable": false, + "synonyms": "oak trunk wood", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "rock", + "sprite": "rock", + "x": 120, + "y": 130, + "look": "A mossy boulder. Immovable, and a little smug about it.", + "takeable": false, + "synonyms": "boulder stone", + "use_text": "", + "needs": "", + "wins": false + }, + { + "name": "sign", + "sprite": "sign", + "x": 80, + "y": 50, + "look": "A weathered signpost, paint long faded.", + "takeable": false, + "synonyms": "notice post", + "use_text": "", + "needs": "", + "wins": false + } +] \ No newline at end of file diff --git a/kit/themes/README.md b/kit/themes/README.md new file mode 100644 index 0000000..0b8911d --- /dev/null +++ b/kit/themes/README.md @@ -0,0 +1,43 @@ +# MRPGI theme packs + +Drop-in archetypes across four genres. Each folder has: +- **`objects.json`** — a catalog of ready-made objects (name / look / use text / `needs` / `wins` / synonyms all written). Copy any into a room. +- **`locations/*.json`** — a sample room you can use as-is or as a template. +- **`gemini_sprites.md`** — Nano Banana prompts to make the art when you have time. + +| theme | vibe | object highlights | +|-------|------|-------------------| +| `scifi/` | derelict-ship sci-fi | console, blaster, alien, robot, keycard, airlock (win) | +| `monsters/` | a bestiary | goblin, slime, troll, ghost, dragon, mimic, skeleton | +| `saucy/` | flirty nightlife comedy | rose, wine, cocktail, jukebox, letter, velvet door (win) | +| `horror/` | gothic candlelit dread | candle, skull, coffin, ouija, portrait, crypt door (win) | + +## Use an object +Open a theme's `objects.json`, copy one `{ … }` block, and paste it into the +`objects: [ … ]` array of any `rooms/roomN.json` (tweak its `x`/`y`). Or place a +sprite with the object tool (`0`) in the editor and copy the look/use/needs text +into its fields. + +## Use a location +```sh +cp kit/themes/horror/locations/crypt.json rooms/room5.json +``` +Launch, press `>` to walk to room 5, then re-wire its **exits** on the top strip +to connect it to your world. (Sample locations ship sealed — you add the doorways.) + +## Make the art (later) +Each `gemini_sprites.md` has a shared STYLE line + a per-sprite table. The flow: +1. Generate on a **solid magenta background** (image models don't do transparency well). +2. Key the magenta to transparent in any editor (magic-wand / "remove background"). +3. Save as `sprites/.png` — the name must match the object's `sprite` field. +4. Press `R` in the editor. Done. + +Until then, objects whose sprite doesn't exist show as a small **magenta square** +placeholder — totally fine for blocking out a game. Many sprites already exist +from the fantasy kit (`orc`, `chest`, `door`, `key`, `torch`, `potion`, …) and are +reused across themes. + +## Note on the EGA palette +Don't stress matching colours exactly — the engine snaps every loaded image to the +16-colour EGA palette automatically. Load `palettes/mrpgi-ega.gpl` in Aseprite if +you want to paint in the exact colours from the start. diff --git a/kit/themes/horror/gemini_sprites.md b/kit/themes/horror/gemini_sprites.md new file mode 100644 index 0000000..4d36dbb --- /dev/null +++ b/kit/themes/horror/gemini_sprites.md @@ -0,0 +1,25 @@ +# Classic horror sprites — Gemini Nano Banana prompts + +Gothic, candlelit, a little funny. Generate each on a **solid flat magenta +(#FF00FF) background**, key out the magenta, drop into `sprites/`, press `R`. +Colours snap to EGA on load. + +**STYLE (prepend to every prompt):** +> 16-colour EGA pixel-art sprite, single object centred on the bottom edge, bold readable silhouette, flat hard-edged colours, desaturated gothic palette — greys, bone-white, sickly green and deep violet, with a single warm candle-amber accent — retro 1980s Sierra/AGI horror look, eerie but a touch comedic, on a solid flat magenta (#FF00FF) background, NO text, NO drop shadow, NO scene. + +| filename | size | subject | +|----------|------|---------| +| `candle` | 8×20 | a black candle with a small amber flame and dripping wax | +| `skull` | 14×12 | a yellowed human skull, grinning, hollow eye sockets | +| `coffin` | 22×14 | a stone/wood coffin, lid slightly ajar | +| `gravestone` | 16×18 | a leaning weathered headstone with a scratched-out name | +| `cobweb` | 16×16 | a corner cobweb, thick lace, one waiting spider | +| `portrait` | 16×20 | an old framed portrait of a pale figure whose eyes seem to follow | +| `clock` | 14×26 | a tall grandfather clock stopped at 3:07, pendulum still | +| `doll` | 12×18 | a porcelain doll in a faded dress, cracked face, blank stare | +| `diary` | 14×12 | a small locked leather diary with an iron clasp | +| `ouija` | 18×14 | a wooden ouija board with a heart-shaped planchette | + +Reuses: `mirror` (from the saucy pack — a foxed antique mirror works fine), `crypt door`→`door`, and `key`. + +**Flat template:** `