From a1f7d8d3966227b66a2dc1eaa458b1395ab83d8d Mon Sep 17 00:00:00 2001 From: m3ultra Date: Sat, 25 Jul 2026 23:47:55 +1000 Subject: [PATCH] =?UTF-8?q?MRP3GI=20spec=20v1.0=20=E2=80=94=203D=20present?= =?UTF-8?q?ation,=202D=20brain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .gitignore | 6 ++ README.md | 35 ++++++ SPEC.md | 309 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 350 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 SPEC.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16525f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +src/wasm/ +bridge/target/ +target/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..c50fed6 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# MRP3GI — Monster Robot Party 3D Game Interpreter + +The **third generation** of the Monster Robot Party engine family: + +| Engine | Sierra analog | The trick | +|---|---|---| +| [MRPGI](https://github.com/) (`~/Documents/mrpgi`) | AGI (1984) | 16-color vector rooms, text parser | +| [MRPCI](https://github.com/) (`~/Documents/mrpci`) | SCI0/SCI1 (1988–92) | 256-color palettes, point-and-click, rhai scripts | +| **MRP3GI** (this repo) | SCI32 / Phantasmagoria-era (1995+) | **3D presentation, 2D brain** | + +One sentence: **the MRPCI story engine, compiled to WASM, drives rigged GLB +characters in a three.js scene** — Grim-Fandango-style: the sim stays a +deterministic 2D adventure engine; the camera, sets, lighting and Mixamo +animation are presentation. + +Read [SPEC.md](SPEC.md) for the full architecture and the milestone plan. +Nothing in this repo reimplements game rules: if it decides whether a door +opens, it lives in `mrpci-core`. If it decides what a door looks like, it +lives here. + +## Prerequisites + +- `mrpci` checked out as a **sibling directory** (`../mrpci`) — the bridge + crate uses a path dependency +- Rust (stable) + `wasm-pack`; Node 20+ + npm +- The demo game: `cd ../mrpci && cargo run -p mrpci-core --bin mrpci-headless -- --sample` + +## Quick start (once M2 lands) + +```sh +npm install +npm run bridge # wasm-pack build of the Rust story core +npm run dev # vite dev server → play Neon Precinct in 3D +npm test # incl. the golden determinism replay vs native +``` diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..d7c28e4 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,309 @@ +# MRP3GI — Specification & Execution Plan v1.0 + +**For the implementing agent:** this document is the contract. Work the +milestones in order; each has acceptance criteria you can verify yourself +(headless where possible). When this spec and convenience disagree, the +spec wins. When the spec is silent, follow the Architecture Invariants. + +--- + +## 1. What this is + +MRP3GI renders classic Sierra-style adventure games in **3D** (three.js, +rigged GLB characters, real camera) while the entire game — rooms, verbs, +hotspots, dialogue, flags, score, timers, death, saves — runs inside +**`mrpci-core`**, the existing Rust SCI-generation engine, compiled to +WASM. This is the Grim Fandango model: 3D presentation over a 2D brain. + +Why this shape: + +- `mrpci-core` is already headless, deterministic (fixed 30Hz ticks, seeded + RNG, no wall clock), and driven entirely by a JSON Command/Event bus. Its + native-only deps (`tiny_http`) are already fenced behind + `cfg(not(target_arch = "wasm32"))`. It was built for this. +- Every MRPCI game (starting with the Neon Precinct demo) becomes a 3D game + for free, and every future MRP3GI game is *also* playable in the 2D + MRPCI GUI, testable over MCP, and replayable in CI. +- The sim's coordinate space is 2D (x 0..320, y 0..190, y = depth). Actors + walk on a ground plane. That is exactly what a 3D adventure needs: the + renderer maps sim (x, y) → world (X, Z) and stands a model there. + +## 2. Architecture invariants (do not violate) + +1. **No game rules in TypeScript.** If it changes game state, it is a + `Command` into the core. If the player learns something, it arrived as + an `Event`. The TS layer may keep *presentation* state only (camera, + animation phase, UI open/closed). +2. **The bridge surface is JSON strings + byte buffers.** No fine-grained + wasm-bindgen types for game objects — the wire format is the same one + the JSONL/HTTP/MCP surfaces already speak (see `../mrpci/docs/CONTROL.md`). +3. **Determinism is a feature.** The golden-replay test (M1) must pass on + every commit: the WASM core, fed the canonical playthrough script, + produces byte-identical event JSON to the native binary. +4. **`mrpci-core` is consumed, not forked.** Path dependency `../mrpci`. + If the core needs a change (it will — see §4.1), make it in the mrpci + repo, keep it native-compatible, run mrpci's own tests, commit there. +5. **60fps presentation, 30Hz sim.** The renderer interpolates actor + positions between sim ticks; it never runs the sim faster to look + smoother. + +## 3. Repo layout + +``` +mrp3gi/ + SPEC.md # this file + README.md + package.json # vite + typescript + three + vitest + vite.config.ts + tsconfig.json # strict: true + bridge/ # Rust: the WASM story core + Cargo.toml # crate mrp3gi-bridge; dep mrpci-core = { path = "../../mrpci/mrpci-core" } + src/lib.rs # wasm-bindgen exports (§4) + src/ + main.ts # boot: load bundle, start loop + bus.ts # typed Command/Event wrappers over the bridge + world3d.ts # scene graph: set, ground, lights, camera rig + mapping.ts # sim-space ↔ world-space (§5) + actors.ts # GLB registry, AnimationMixer state machines (§7) + sets.ts # backdrop + control-screen extrusion (§6) + picking.ts # raycast → sim coords → VerbAt/MoveTo + ui.ts # HTML overlay: verb bar, dialogue, inventory, log + audio.ts # WebAudio sink for cue/music events (§8) + interp.ts # per-actor position/facing interpolation + public/ + models/ # GLBs (robot.glb first; from the MeshGod/Mixamo pipeline) + games/neon-precinct.bundle.json # generated (§4.2) + tests/ + golden.test.ts # determinism replay vs recorded native events + mapping.test.ts # coordinate round-trips + scripts/ + make-bundle.mjs # game folder → bundle JSON (calls native headless) + record-golden.mjs # regenerates tests/golden.events.jsonl from native +``` + +## 4. The WASM bridge + +### 4.1 Prerequisite changes in `../mrpci` (small, do these first) + +1. **`Event` needs `Deserialize`** (tests compare parsed events) — add + `Deserialize` to the derive on `Event` and its payload structs, or keep + comparison as raw strings (acceptable; then skip this). +2. **Bundle must carry everything a filesystem-less build needs.** + `WorldBundle` already carries manifest + rooms + scripts. Add + `sprites: HashMap rgba)>` (serde with + base64 or plain array) populated by `World::to_bundle()`, and make + `World::from_memory`/`apply_bundle` accept them. Background `pics/` + PNGs may be skipped in v1 — Neon Precinct is fully procedural. +3. **Expose a screens accessor** if not public already: the bridge needs + `visual/priority/control/hotspot` slices plus `render_indices()` and + `effective_palette()` (both exist). +4. Confirm `rhai` builds for `wasm32-unknown-unknown` with the existing + `["sync", "no_time"]` features (it does; `no_time` exists precisely for + wasm). `image` crate is PNG-only and wasm-safe. + +### 4.2 Bundle format + +`scripts/make-bundle.mjs` runs the native `mrpci-headless` (build it from +`../mrpci`) with a new tiny flag to add there: `--export-bundle out.json` +(serializes `world.to_bundle()`, including sprites per 4.1.2). The bundle +is committed under `public/games/` so the web app needs no Rust at runtime. + +### 4.3 `bridge/src/lib.rs` exports (complete list) + +```rust +#[wasm_bindgen] +pub struct Engine { gs: GameState } + +#[wasm_bindgen] +impl Engine { + /// Boot from a bundle JSON string. Never panics: bad JSON → Err(String). + #[wasm_bindgen(constructor)] + pub fn new(bundle_json: &str) -> Result; + + /// One Command JSON in → JSON array of Events out. + pub fn apply(&mut self, command_json: &str) -> String; + + /// Wall-clock driver: accumulates dt (seconds) into fixed 30Hz cycles. + pub fn tick(&mut self, dt: f32) -> String; // events JSON + + pub fn snapshot(&self) -> String; // StateSnapshot JSON + + // --- presentation feeds (no JSON; raw buffers, zero-copy where easy) --- + pub fn render_indices(&self) -> Vec; // 320*190 palette indices (scene+weather) + pub fn effective_palette(&self) -> Vec; // 256*4 RGBA, cycle LUT applied + pub fn control_screen(&self) -> Vec; // walls/water (set extrusion + debug) + pub fn priority_screen(&self) -> Vec; + pub fn hotspot_screen(&self) -> Vec; + + /// Actor transforms for the renderer, cheaper than full snapshots: + /// JSON [{name, x, y, dir, scale, walking, visible, talking}] — ego first. + pub fn actors(&self) -> String; + + // --- audio content (bytes are WAV; browser decodes once, caches) --- + pub fn cue_wav(&self, name: &str) -> Vec; + pub fn music_wav(&self, mood: u8) -> Vec; +} +``` + +Notes: +- `actors()` needs a small core addition or can be assembled from + `snapshot()` + ego fields; prefer a dedicated method (cheap, per-frame). + Include NPCs and the ego with their current `dir` and `walking` flags — + the animation state machine keys off these. +- Build with `wasm-pack build bridge --target web --out-dir ../src/wasm`. + Commit nothing from `src/wasm` (gitignore); `npm run bridge` regenerates. + +## 5. Coordinate mapping (`mapping.ts`) + +- `PIC_W = 320`, `PIC_H = 190`, `UNITS_PER_PIXEL = 1/32`. +- Sim `(x, y)` → world `(X, 0, Z)`: `X = (x - 160) * UPP`, + `Z = (y - 190) * UPP` (so the room front edge is Z = 0, deeper rooms go + negative Z; +Y is up). Inverse for picking; round-trip test required. +- Character height: sim actors are ~36px tall ⇒ target GLB display height + `36 * UPP * heightScale(actor)`; the room `ScaleTable` is **ignored for + size** in 3D (real perspective does that job) but `scale()` still feeds + walk-speed in the core — leave the core alone. +- Camera rig per room (presentation data, §6.3): default is a SCI-style + fixed camera at `(0, 2.2, 1.8)` looking at `(0, 0.5, -2.2)`, vertical + FOV 50°. All values overridable per room. + +## 6. Sets (three phases, shipped in this order) + +### 6.1 Backdrop mode (M2) +The room's own 256-color frame becomes the set: +- Ground plane: `PIC_W*UPP × PIC_H*UPP`, textured with the + `render_indices` + palette composite (build an offscreen canvas texture; + `NearestFilter`, `SRGBColorSpace`). Rebuild the texture only when a + `room_changed` event arrives or every N frames while palette cycles run + (cheap: 60KB). +- Backdrop plane: same frame, standing at the room's far edge, so the + camera sees painted walls behind the 3D actors. Crude and charming; it + proves the whole pipe. + +### 6.2 Extruded blockout (M4) +Generate set geometry from the invisible screens: +- March the **control screen**: cells with `CTL_BLOCK` become merged boxes + (greedy meshing, 4px grid resolution), height 1.4 world units, textured + by sampling the visual screen at the wall's base row. Water cells get a + translucent blue plane at y=0.02. +- The visual frame still textures the ground; walls now occlude actors in + real 3D — the priority screen's job, inherited by the depth buffer. +- Hotspot debug view (`?debug=hotspots`): tinted transparent prisms. + +### 6.3 Authored sets + camera (M5) +`public/games/.3d.json` (presentation-only, never in the core): +```json +{ + "rooms": { + "0": { + "camera": { "pos": [0,2.2,1.8], "lookAt": [0,0.5,-2.2], "fov": 50 }, + "set": { "glb": "models/sets/neon-row.glb" }, + "lights": [ {"type":"point","pos":[1.2,1.5,-2],"color":"#ff8844","intensity":2} ], + "actors": { "vend-bot": "models/vendbot.glb" } + } + }, + "actors": { "ego": "models/robot.glb", "sergeant": "models/sergeant.glb" } +} +``` +Missing entries fall back to 6.2, then 6.1. A GLB set replaces extruded +geometry but the control screen **still owns collision** — sets are looks. + +## 7. Actors (`actors.ts`) + +- Loader: GLTFLoader + DRACO optional. Registry maps actor name → model + URL via `.3d.json`, falling back to `models/robot.glb`, falling back to + a capsule + name sprite (never crash on a missing model — placeholder + and console warn, MRPCI's no-Error-47 rule extends to assets). +- Clips: models follow the house Mixamo convention — clips named `idle`, + `walk`, `talk` (extras ignored in v1). State machine per actor: + `walking → walk`, `in_dialogue with this actor → talk`, else `idle`; + crossfade 0.15s. +- Facing: sim `dir` (1..8) → yaw; when walking, face the interpolated + velocity vector instead (smoother on A* paths); damp yaw at 10 rad/s. +- Interpolation (`interp.ts`): keep the last two sim positions per actor + with their tick stamps; render at `renderTime - one tick` with linear + interpolation. Teleports (place_ego, room change) snap: distance > 24px + in one tick ⇒ no lerp. +- Weather: `room_changed` carries the room; read `weather` from snapshot. + Rain/snow/embers as a THREE.Points system (~300 particles) matching the + core's look. Purely visual; do not consume core RNG (mirror of the + particles.rs rule). + +## 8. UI & audio + +- **Verb bar** (HTML/CSS overlay, not WebGL): WALK/LOOK/DO/TALK + BAG + + score + room name. Right-click cycles verbs. Number keys answer + dialogue. Enter opens the parser line → `{"cmd":"parse"}` — the text + parser ships in 3D too, because it's free. +- **Dialogue**: `dialogue_open` events render the classic window as HTML + (white, red border); choices clickable. +- **Picking**: raycast the ground plane + extruded walls + actor meshes. + Actor hit → `verb_at` at that actor's sim coords; ground hit → inverse + mapping → `move_to`/`verb_at`. Walls hit → the sim coords of the wall + base pixel (hotspots live on the control/hotspot screens there). +- **Audio**: on `audio`/`music` events, decode `cue_wav`/`music_wav` bytes + through WebAudio (decode once, cache buffers; music loops, ~0.13 gain). + Honor a mute toggle. Files under `/sfx|music/` may override later + — v1 uses the synth only. +- **Transcript log**: last 3 lines, bottom overlay, fading. + +## 9. Milestones & acceptance criteria + +**M0 — scaffold.** vite+TS strict+three+vitest; `bridge/` compiles with +wasm-pack; `npm run bridge && npm run dev` shows a lit empty scene and an +FPS meter. CI script (plain `npm test`) green. + +**M1 — the brain in the browser.** mrpci changes from §4.1 landed (in +`../mrpci`, its tests still green). Bundle exporter works; Engine boots +Neon Precinct in a vitest node environment; **golden test**: running +`tests/playthrough.jsonl` (copy the canonical one from the mrpci session +logs or re-record with `scripts/record-golden.mjs`) through the WASM +Engine produces event JSON byte-identical to `tests/golden.events.jsonl` +recorded from the native binary. This test is sacred; it runs forever. + +**M2 — backdrop 3D.** Ground+backdrop textured from the live frame; +capsule actors move when you click; edge exits and the precinct door +change rooms (with a fade); verb bar works; you can play start-to-finish +(win 25/25) with capsules. Weather points visible in rooms 0 and 2. + +**M3 — rigged robots.** `robot.glb` (produce with the existing +MeshGod/Mixamo pipeline; idle/walk/talk clips) replaces the ego capsule; +NPC models or recolored fallbacks for sergeant/vend-bot; animation state +machine + interpolation + facing damping feel right at 60fps; dialogue UI +styled; full playthrough again, now looking like a game. + +**M4 — extruded sets + lighting.** Control-screen greedy meshing; walls +occlude actors; per-room ambient+key light defaults derived from the +room's palette average; `?debug=` views for control/hotspot/priority. + +**M5 — authored polish + ship.** `.3d.json` camera/lights/models per +room for Neon Precinct; save/restore UI over core SaveGame/RestoreGame +(slots in the core's save dir don't exist on web — add bridge methods +`save_json()`/`restore_json(s)` wrapping SaveData to/from localStorage); +`npm run build` produces a static bundle; deploy per the fleet deploy-map +(games VPS / partly.party, dockerized like the other web games). Run +`/ship-check` before exposing. + +## 10. Testing bar + +- `golden.test.ts` — determinism (M1, forever). +- `mapping.test.ts` — sim↔world round-trips, edge pixels. +- A `smoke.test.ts` that boots Engine, ticks 300, asserts no `error` + events from scripts and a stable snapshot shape. +- Visual checks: puppeteer screenshot script (`npm run shot -- --room 2`) + for eyeballing; not CI-gating in v1. + +## 11. Out of scope for v1 (do not build yet) + +Free camera / player-relative controls; physics; navmesh (the control +screen *is* the navmesh); shadows beyond a cheap blob; mobile controls; +multiplayer; asset streaming; a TS reimplementation of anything the core +does. + +## 12. Open questions (decide during M3, note decisions in this file) + +- Lip-flap/talk loop timing from transcript length? +- Palette-cycling neon in 3D: emissive planes sampling the cycled palette + vs. baked flipbook — try emissive planes first. +- Whether extruded walls should sample the *backdrop* texture region + instead of base-row color (probably yes, later).