MRP3GI spec v1.0 — 3D presentation, 2D brain
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
a1f7d8d396
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
src/wasm/
|
||||
bridge/target/
|
||||
target/
|
||||
.DS_Store
|
||||
35
README.md
Normal file
35
README.md
Normal 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 (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
|
||||
```
|
||||
309
SPEC.md
Normal file
309
SPEC.md
Normal 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).
|
||||
Loading…
Reference in New Issue
Block a user