Add parallel-lane execution specs for Opus 4.8

LANES/PLAN.md         — verdict, GODVERSE pipeline architecture, shared contracts, 3GOD security note
LANES/LANE1-*.md      — Godot: grab/slide/smash record ritual + find-the-misfiled-disc mode (owns game/)
LANES/LANE2-*.md      — meshgod cell-fracture stage + Destroyulator prop batch -> 3GOD (owns ~/meshgod)
LANES/LANE3-*.md      — thriftgod-based web smash toy, Rapier physics, 3GOD props (owns web/)
LANES/KICKOFF.md      — per-lane opening prompts + collision-safety rules

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Monster Robot Party 2026-07-14 16:47:47 +10:00
parent 5852322606
commit c03bb85c9f
5 changed files with 331 additions and 0 deletions

36
LANES/KICKOFF.md Normal file
View File

@ -0,0 +1,36 @@
# KICKOFF — how to run the three lanes in parallel
Read `PLAN.md` first (verdict, architecture, shared contracts, the 3GOD security note).
Then start each lane as its own Opus 4.8 session with the one-liner below. Each session's
FIRST job is to read its lane file + `PLAN.md`, then execute.
## Start each lane (paste as the session's opening prompt)
**Lane 1 — Godot record ritual**
> Read `~/Documents/Destroyulator/LANES/PLAN.md` then `LANES/LANE1-godot-record-ritual.md` and
> execute it fully. You own `game/` only. Build the grab→slide→smash record ritual + the
> "find the misfiled disc" mode. Pass the headless smoke test before every commit; commit +
> push to `origin main` as you go.
**Lane 2 — Asset pipeline / fracture**
> Read `~/Documents/Destroyulator/LANES/PLAN.md` then `LANES/LANE2-asset-pipeline-fracture.md`
> and execute it. You own `~/Documents/meshgod` (a separate repo). Add the cell-fracture stage,
> build the Destroyulator prop manifest, generate + fracture + push to 3GOD. CHECK THE fal
> CREDIT before any big batch; do the top-6 props first if it's tight. Commit to meshgod's remote.
**Lane 3 — Web smash toy**
> Read `~/Documents/Destroyulator/LANES/PLAN.md` then `LANES/LANE3-web-smash-toy.md` and execute
> it. You own `web/` (new dir in the destroyulator monorepo). Fork thriftgod's FP interior into a
> browser smash toy with Rapier physics, 3GOD props, and a juice layer. Do NOT edit thriftgod.
> Commit + push to `origin main` under `web/`.
## Collision safety
- Lane 1 → writes `game/` only. Lane 3 → writes `web/` only. Lane 2 → writes `~/Documents/meshgod`
only (never `game/`; it delivers assets via 3GOD, and Lane 1 pulls approved GLBs in itself).
- All push to the same remote `ssh://git@100.71.119.27:222/monster/destroyulator.git` (Lanes 1 & 3)
— different top-level dirs, so no merge conflicts. Lane 2 pushes to its own meshgod remote.
- If two lanes ever need the same file, STOP and coordinate through the human — don't guess.
## Definition of done (whole plan)
Godot game has the record ritual + a playable mode; meshgod can fracture and has shipped the
prop batch to 3GOD; the web toy is playable in-browser. Each lane green on its own acceptance test.

View File

@ -0,0 +1,87 @@
# LANE 1 — Godot: the record-destruction ritual + "find the misfiled disc" mode
**Owns:** `game/` only. **Engine:** Godot 4.7 (Jolt). **For:** an Opus 4.8 session.
**Runs on:** M3 Ultra, `/Applications/Godot.app`.
## Objective
Turn records from one-hit props into the game's signature **dexterity ritual**:
> look at a record → **Grab (E)** to lift the sleeve out of the crate → **drag mouse
> left↔right** to slide the black disc out of the sleeve → the freed disc is a throwable
> brittle object (**LMB throw / smash**), and the empty sleeve is separately trashable.
Then a first game mode on top of it: **"Find the Misfiled Disc"** — one disc is in the
wrong sleeve; find it before the timer, and the frantic search leaves a mess (the point).
## What already exists (read these first)
- `game/scripts/Player.gd` — FP `CharacterBody3D`: mouse-look, WASD, `camera`, punch/kick
via `MeleeAttack`. Add the grab controller here (or a child node it owns).
- `game/scripts/Smashable.gd``class_name Smashable extends RigidBody3D`; `kind`,
`PROFILES`, `smash(impulse)`, `shatter()`, `supports[]`/`release()` cascade, `smashed` signal.
- `game/scripts/Main.gd` — builds the store (`_glb_piece()` auto-fits colliders, drops to
floor), `_skin_record()` UV-tiles sleeve art, HUD, `B`=rain, `R`=reset. `_on_smashed`→`Juice`.
- `game/scripts/Juice.gd` — hitstop/shake/particles/procedural audio, `impact(kind,pos,shards)`.
- Assets: `game/assets/store/record.glb` (sleeve, carries `M_Sleeve_Front/Back` mats).
Disc-only + sleeve-only source GLBs exist at `~/Documents/Destroyulater/3D-STORE/clean_glbs/`
(`VinylDisc_v4.glb`, `VinylSleeve_v4.glb`) — copy into `game/assets/store/` if you split them.
## Deliverables
### 1. `game/scripts/Record.gd``class_name Record extends RigidBody3D`
A record = a **cardboard sleeve** (this body, `kind="cardboard"` Smashable-like) that
**contains a hidden vinyl disc**. State machine: `IN_CRATE → HELD → EXTRACTING → DISC_FREE`.
- Exports/fields: `sleeve_genre: String`, `disc_genre: String` (usually equal; the mode
sets one mismatch), `_extract: float` (0→1 pull progress), `_disc: RigidBody3D` (the disc,
starts frozen inside the sleeve at local offset).
- `grab(hold_point: Node3D)`: freeze, reparent-follow the player hold point (kinematic).
- `set_extract(amount: float)`: clamp 0..1, lerp `_disc` local position along the sleeve's
+X (opening) axis by `amount * sleeve_length`; peek the disc label as it emerges.
- At `_extract >= 1.0`: `release_disc()` → unfreeze `_disc`, detach as free brittle vinyl
`Smashable`, keep the now-empty sleeve as a trashable cardboard body.
- The disc + sleeve both reuse `Smashable`'s shatter/juice path (emit `smashed`).
### 2. `game/scripts/GrabController.gd` — attach under `Player`
- A hold point (`Node3D` child of camera, ~0.5m forward, ~-0.1 down).
- `E` = grab the `Record` under the crosshair (reuse the sphere/ray query pattern from
`MeleeAttack.strike`, but filter for `Record`). Only grabbable within reach (~2m).
- While held: accumulate `InputEventMouseMotion.relative.x` into the held record's
`set_extract()` (tune: ~600px of drag = full pull). Show an on-screen extract meter.
- `LMB` while holding a freed disc = throw (impulse along camera forward, disc is brittle
so it cracks on impact). `G` or `LMB` on the empty sleeve = drop/trash it.
- `Esc` still releases mouse (don't break existing Player input).
### 3. `game/scripts/GameMode.gd``class_name GameMode extends Node`, owned by `Main`
- **FreePlay** (default) and **FindMisfiled**.
- FindMisfiled: on start, pick N records, set exactly ONE to `disc_genre != sleeve_genre`
(e.g. a techno disc in a jazz sleeve). Start a 60s timer (HUD). Win = the player extracts
the misfiled disc and presses **F (report)** while holding it. Track "records disturbed"
and "mess score" (bodies knocked / smashed via `_on_smashed`). Show result on win/lose.
- Key `1` = FreePlay, `2` = start FindMisfiled, on the existing `Main._unhandled_input`.
### 4. Wire into `Main._build_rack`
- Replace the `_glb_piece(RECORD_GLB, "vinyl", …)` record placement with `Record` instances
(sleeve+disc). Keep them stacked on crates; keep `crate.supports.append(record)`.
- Keep `_skin_record()` for the sleeve art; add a small disc-label tint by `disc_genre`.
## Acceptance (must pass before each commit)
```sh
# zero script errors across 180 frames (2 "material is null" lines are benign headless noise)
/Applications/Godot.app/Contents/MacOS/Godot --headless --path game --quit-after 180 2>&1 \
| grep -E 'SCRIPT ERROR:|Parse Error' | grep -v 'material.*null' # must be empty
```
Plus a `print()` on mode start confirming the misfiled record was planted (genre A in genre B).
Then eyeball in-editor: grab a record, drag to pull the disc, throw it, trash the sleeve.
## Gotchas
- Held bodies: set `freeze=true` + move via `global_transform` each `_physics_process`, OR
use a `Generic6DOFJoint3D`/`PinJoint3D` to the hold point — simpler is freeze+follow.
- Don't let the disc's brittle `body_entered` fire while it's still inside the sleeve
(guard: only arm brittleness after `release_disc()`).
- Keep everything in groups `smashable`/`debris` so the HUD body count + `Juice` keep working.
## Definition of done
Grab/lift/slide/smash works end-to-end; FindMisfiled mode is playable start→win/lose with a
timer and mess score; headless smoke test clean; committed + pushed to `origin main`.

View File

@ -0,0 +1,74 @@
# LANE 2 — Asset pipeline: cell-fracture stage + Destroyulator prop batch
**Owns:** `~/Documents/meshgod` (SEPARATE repo — not the destroyulator monorepo).
**For:** an Opus 4.8 session. **Runs on:** M3 Ultra; Blender at `/Applications/Blender.app`.
## Objective
Give the meshgod factory a **fracture stage** so props arrive pre-shattered for smashing,
then batch-generate Destroyulator's workplace props and push them to 3GOD. This upgrades
Lanes 1 & 3 from "single-box smashable" to real shard-by-shard destruction.
## What already exists (read first)
- `meshgod/finish.py` + `scripts/finish_glb.py` — the **headless-Blender subprocess pattern
to copy**: launches `/Applications/Blender.app` `--background --python <script> -- <args>`,
parses a `FINISH_RESULT:{json}` line from stdout. Finishing does join→solidify (watertight!)
→decimate→brighten→normalize→export GLB.
- `meshgod/batch.py` — resumable manifest runner (`python3 -m meshgod.batch manifest <f.json>
--out <dir>`, content-hashed IDs, checkpoints, `--workers`).
- `meshgod/pipeline.py`, `config.py` — the OpenRouter→fal.ai→finish flow. **Cost is real**
(~$0.100.55/asset); fal key in gitignored `open.txt`. Check remaining credit before a big batch.
- 3GOD push: `MESHGOD/scripts/test_3god_push.py` (on M1) guards the depot auth/slug contract;
find/mirror the actual push helper meshgod uses to POST a finished GLB to digalot.fyi/3god.
## Deliverables
### 1. `meshgod/scripts/fracture_glb.py` — headless Blender cell-fracture
Mirror `finish_glb.py`'s structure exactly (arg parsing, `FRACTURE_RESULT:{json}` stdout line).
- Input: a finished watertight GLB + `--chunks N` (default 12) + `--out <path>`.
- Enable the **Cell Fracture** addon (`bpy.ops.preferences.addon_enable(module="object_fracture_cell")`)
and run `bpy.ops.object.add_fracture_cell_objects(source_limit=N, ...)` on the imported mesh.
(Blender 5.1 — verify the addon module name; if renamed/removed, fall back to a Voronoi
geometry-nodes or `boolean`-based split, but Cell Fracture is preferred.)
- Each chunk: recenter its origin to its own bounds center (so it can become a rigid body),
keep the parent's material.
- Export **all chunks as a single `<name>.fractured.glb`** whose root's direct children are the
chunk meshes named `chunk_00…chunk_NN` (SHARED-CONTRACT #2 in PLAN.md). Report chunk count +
whether any chunk is non-manifold.
### 2. `meshgod/fracture.py` (module) + CLI
`python3 -m meshgod.fracture <in.glb> --chunks 12 --out <out.fractured.glb>` — thin wrapper
that subprocess-launches the script (same pattern as `finish.py`). Optionally add a
`--fracture` flag to `batch.py` so a manifest entry can request `"fracture": {"chunks": 12}`
and get both `<name>.glb` + `<name>.fractured.glb` out.
### 3. `meshgod/manifests/destroyulator.json` — the prop batch
Workplace hero props, real-metre `height_m` + tri budgets. Include at least:
office **printer** (the boss!), office desk, monitor/CRT, desk phone, filing cabinet,
water cooler, office chair, ceiling fluorescent light, coffee mug, potted plant, wall clock,
cardboard box; record-store extras: turntable, amp/speaker, cash register, wall shelf.
Each entry: `{name, prompt, height_m, tris, fracture:{chunks}}`. ~16 assets ≈ $38, ~30 min.
**Confirm the fal budget covers it before running; if low, do the top 6 first.**
### 4. Push finished + fractured GLBs to 3GOD
Use meshgod's existing depot-push path. Land both the intact and `.fractured.glb` on the
digalot.fyi/3god shelf so Lanes 1 & 3 can pull them by slug.
## Acceptance
- `fracture_glb.py` on one finished GLB (e.g. `outputs/robot_game.glb`) yields a
`.fractured.glb` with ≥8 chunk children named `chunk_*`; verify by loading it in Blender
headless and printing child count + names.
- The manifest runs (or a 2-asset subset does) end-to-end: intact + fractured GLBs on disk,
watertight, correct scale, pushed to 3GOD (`GET /api/list` shows the new slugs).
## Gotchas
- Cell Fracture needs the mesh to be a single manifold object; run it AFTER finish's solidify.
- fal costs money per asset and the key is shared/rotating — read `open.txt`, don't hardcode,
and cap batch size to the remaining credit.
- Keep chunk counts modest (816). Hundreds of chunks tank the consuming physics engines.
## Definition of done
Fracture stage works and is documented; `destroyulator.json` (or its top-6 subset) generated,
finished, fractured, and pushed to 3GOD; committed + pushed to meshgod's own remote.
Post the list of produced slugs so Lanes 1 & 3 can consume them.

View File

@ -0,0 +1,75 @@
# LANE 3 — Web smash toy: thriftgod-based FP demo, 3GOD props, Rapier physics
**Owns:** `web/` (new dir in the destroyulator monorepo). **For:** an Opus 4.8 session.
**Runs on:** M3 Ultra (serve locally); deploy target later is the digalot.fyi VPS.
## Objective
A **shareable, instant-link web toy** — not a port of the Godot game, a different product in
the same universe. Walk around a room first-person, grab records, and smash the place, in the
browser. Built on thriftgod's proven FP-interior code, physics via Rapier (WASM), props pulled
live from the 3GOD depot. This is the streamer-bait / "try it in one click" front door.
## What already exists (study, then fork — DON'T edit thriftgod)
- `~/Documents/thriftgod` (local) — LIVE Three.js first-person game (digalot.fyi). Copy the
patterns, don't modify it. Reusable pieces:
- `web/index.html` (~129k) — pointer-lock WASD FP controller, seeded procedural interior,
3D pickup/inspect interaction, **GLTFLoader + DRACOLoader** wiring (~line 1233, fixture
kind `'glb'`). This is your movement + loading foundation.
- `web/dig.js`, `web/cards.js` — interaction + item UI patterns.
- Three.js r0.175 via unpkg importmap, no build step (keep this — zero-tooling is a feature).
- `~/Documents/Destroyulater/game/scripts/dig.js` reference (recordgod) — procedural Web Audio
"fwip/thunk"; reuse the synth approach for smash SFX (mirror Godot's `Juice.gd` material voices).
- 3GOD depot: `https://digalot.fyi/3god`, `GET /api/list` → shelf JSON; GLBs by slug. **Read-only.**
## Deliverables (all under `web/`)
### 1. `web/index.html` + `web/main.js` — the app
- Fork thriftgod's pointer-lock FP controller + a simple room (floor, walls, a couple of racks).
- Load props from 3GOD by slug via GLTFLoader (start with `record`, `crate`, `milk-crate-stack`,
`vintage-cash-register`, and the Lane-2 printer once it lands). Fallback to a local
`web/assets/` copy if the depot is unreachable, so the demo never hard-fails offline.
### 2. `web/physics.js` — Rapier (WASM) destruction
- Use `@dimforge/rapier3d-compat` (or `-simd`) via CDN/ESM. One rigid body per smashable prop,
colliders auto-sized from the GLB bounding box (mirror Godot's `_body_local_aabb` approach).
- **Support-graph cascade** — port the Godot idea: frozen (fixed) bodies hold up dynamic ones;
destroying a support switches its dependents to dynamic. Same "knock the legs, it pancakes" feel.
- **Pre-fractured swap:** when a prop has a `.fractured.glb` (Lane 2 contract #2), on smash hide
the intact mesh and spawn each `chunk_*` as a short-lived dynamic body inheriting velocity.
Brittle kinds (vinyl/glass) also shatter on hard impact. Pool + despawn debris (JS GC is the
ceiling — cap active bodies, sleep/remove settled ones).
### 3. `web/juice.js` — feel layer
- Screenshake (camera trauma), a brief hitstop (scale the physics step / freeze a few frames),
per-material particle bursts (Points or instanced quads), and **procedural Web Audio** smash
SFX per material (wood crunch, cardboard rip, vinyl crack+warble, glass tinkle, steel ring) —
same recipe as Godot `Juice.gd::_synth`. Combo counter in the DOM HUD.
### 4. Record ritual (lite) + share hook
- A simplified version of Lane 1's ritual: click a record to pull it (drag to slide the disc
out), then throw/smash. Keep it snappy for web.
- A "share your mess" button: screenshot the canvas (`toDataURL`) + a smash-count caption.
## Serve + acceptance
```sh
cd web && python3 -m http.server 8099 # then open http://localhost:8099
```
- Loads with no console errors; FP movement + pointer lock work; at least one 3GOD prop loads
and smashes with particles + sound; holds ~60fps with a roomful of props on the M3 (test in
Chrome AND Safari — Safari is the weakest WASM/WebGPU target).
- Cross-origin note: fetching GLBs from digalot.fyi needs CORS on 3GOD; if blocked, mirror the
handful of props into `web/assets/` and load locally for now (flag it for the depot owner).
## Gotchas
- Don't import a bundler unless necessary — thriftgod's no-build importmap approach is the norm here.
- Rapier is WASM: `await RAPIER.init()` before creating the world; keep physics off the render
thread if it janks (Web Worker + SharedArrayBuffer needs COOP/COEP headers — only if needed).
- The 3GOD SSRF/open-mode issue (see PLAN.md) is a WRITE-side problem; this lane only reads, so
it's unaffected — but don't add any write calls to the depot from the web client.
## Definition of done
A playable `web/` smash toy: walk, grab a record, smash props (fractured where available) with
juice, in Chrome + Safari, props from 3GOD (or local mirror). Committed + pushed to `origin main`
under `web/`. Post a localhost screenshot/gif.

59
LANES/PLAN.md Normal file
View File

@ -0,0 +1,59 @@
# DESTROYULATOR — Build Plan & Lane Specs
*Fable's review + plan, 2026-07-08. Three parallel lanes over one shared asset pipeline.*
## The verdict
Destroyulator plugs into the **GODVERSE pipeline you already have**, it does not reinvent it:
```
meshgod ──makes──▶ 3GOD depot ──serves──▶ { Godot game, web toy }
(text→GLB) (digalot.fyi/3god) (two front-ends, one universe)
```
- **Engine question, settled:** Godot stays the shippable game (native Apple Silicon,
Jolt, exports Win/Mac/Linux off the M3). Three.js is NOT a port of the same game —
it's a **separate, shareable web toy** built on thriftgod's live first-person interior
code, wired to the same props. Different product, same universe. Both consume the
same pipeline, so the expensive part (assets) is built once.
- **Assets are hybrid-cloud, not free:** meshgod = OpenRouter + fal.ai (~$0.100.55/asset,
12 min). Budget per batch; don't assume infinite regeneration.
## The three lanes (fire in parallel)
| Lane | Repo / dir it OWNS | Deliverable | Depends on |
|---|---|---|---|
| **1 — Godot: record ritual** | `game/` (this repo) | Grab→lift→slide-disc→smash mechanic + "find the misfiled disc" mode | nothing (starts now) |
| **2 — Asset pipeline** | `~/Documents/meshgod` (separate repo) | Cell-fracture stage + Destroyulator prop manifest → 3GOD | nothing (starts now) |
| **3 — Web smash toy** | `web/` (this repo, new dir) | thriftgod-based FP smash demo, 3GOD props, Rapier physics | 3GOD (read-only) |
**Scope rule (prevents collisions):** each lane writes ONLY inside the dir it owns.
Lane 2 delivers assets to 3GOD + meshgod outputs; Lane 1 *pulls approved GLBs into
`game/assets/`* itself (Lane 2 never writes `game/`). Lane 3 fetches from 3GOD at runtime.
## Shared contracts (every lane obeys these)
1. **GLB convention:** glTF **Y-up, 1 unit = 1 m, origin at floor-center**, textures
embedded, decimated to tri budget. (meshgod already emits this.)
2. **Fracture format:** an intact `<name>.glb` PLUS a sibling `<name>.fractured.glb`
whose root's direct children are the chunk meshes (each a `MeshInstance`/mesh node
named `chunk_00…chunk_NN`, origin at its own centroid, in the intact model's local
space). Consumers instance it and turn each chunk into a rigid body. Defined by Lane 2,
consumed by Lanes 1 & 3.
3. **Depot:** 3GOD at `https://digalot.fyi/3god``GET /api/list` returns the shelf;
GLBs fetchable by slug. Web lane reads live; game lane commits offline copies.
## ⚠️ Pre-req / ops risk (owner's call — NOT auto-fixed)
The review of 3GOD found the **public digalot.fyi/3god instance running in open mode
(`GOD3_OPEN=1`)** — write endpoints (upload/delete/fetch) are reachable **unauthenticated**,
plus an **SSRF hole in `/api/fetch`** (`server.py:173`, reads `file://` + cloud metadata).
Lane 3 only *reads* the depot, so it's not blocked — but before leaning on 3GOD as public
game infrastructure, close open-mode + allow-list `/api/fetch` schemes/hosts. This is a
production box; flagged for you to action, not silently changed.
## Sequencing note
All three start immediately. Lane 2's fracture output *upgrades* Lanes 1 & 3 (real
shard-by-shard destruction) but neither is blocked waiting for it — both already work
with primitive/auto-collider fallbacks. Integrate fractured assets when they land.