MRP3GI spec v1.0 — 3D presentation, 2D brain

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-25 23:47:55 +10:00
commit a1f7d8d396
3 changed files with 350 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules/
dist/
src/wasm/
bridge/target/
target/
.DS_Store

35
README.md Normal file
View File

@ -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 (198892) | 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
```

309
SPEC.md Normal file
View File

@ -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<String, (u32 w, u32 h, Vec<u8> 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<Engine, JsError>;
/// 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<u8>; // 320*190 palette indices (scene+weather)
pub fn effective_palette(&self) -> Vec<u8>; // 256*4 RGBA, cycle LUT applied
pub fn control_screen(&self) -> Vec<u8>; // walls/water (set extrusion + debug)
pub fn priority_screen(&self) -> Vec<u8>;
pub fn hotspot_screen(&self) -> Vec<u8>;
/// 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<u8>;
pub fn music_wav(&self, mood: u8) -> Vec<u8>;
}
```
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/<game>.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 `<game>/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).