Workshop plan + lane docs I (asset runtime) and J (editor)

Custom-GLB drop-in system: manifest/registry with procedural fallbacks,
IndexedDB override layer for zero-deploy playtesting, and a three-panel
in-browser fitting editor. Physics/visual decoupling makes every mesh
swappable; blob.body is the one paintable special case (UV requirements).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-18 15:30:09 +10:00
parent 0f2aca542b
commit cabc60def8
3 changed files with 202 additions and 0 deletions

77
docs/WORKSHOP-PLAN.md Normal file
View File

@ -0,0 +1,77 @@
# BLOBBO Workshop — custom assets + in-game editor (plan, 2026-07-18)
**Goal:** John designs/builds models (Blender, or farm-generated GLB) and drops
them into the live game without touching code — plus an in-browser editor to
preview, fit, and export the mapping.
## Why this is easy here
Physics and visuals are fully decoupled: every collider is a primitive; every
mesh is cosmetic. Swapping a mesh can never break gameplay. The ONE special
case is the blobbo body — the paint system stamps into a canvas texture through
the mesh's UVs — so a custom blob needs clean, non-overlapping UVs and its
material's `map` slot handed to `PaintSkin`.
## Reskinnable slots (the inventory)
| Slot id | Today | Notes |
|---|---|---|
| `blob.body` | UV sphere | PAINTABLE — needs clean UVs; SkinnedMesh OK (idle/wobble clip plays via mixer; squash still applies at group level) |
| `blob.face` | procedural googly eyes | separate from paintable body, by design |
| `ghost.body` | translucent blob copy | auto-derived from blob.body (desaturated) |
| `cannon.base` / `cannon.barrel` | cylinders | per-colour tint applied on top |
| `machine.plate` | box + base | pressure plate |
| `machine.boot` | boot+spring primitive | farm GLB already exists (`assets/meshes/prop-spring-boot.glb`) |
| `machine.bucket` | cylinder shell + fill | farm GLB exists (`prop-paint-bucket.glb`) |
| `machine.arch` | frame + bubbles | bubble cleanser |
| `machine.belt` | striped box | conveyor |
| `course.scenery.*` | giant boxes (cereal box etc.) | free-place decorative slots; toaster GLB exists |
| `course.tramp` | orange pad | gap launcher |
| `course.tunnel` | slab + posts | MINI tunnel |
| `course.finish` | pink podium | finish pad |
| `fx.puddle` | flat glossy slab | material/decal upgrade slot |
| UI: title wordmark, HUD | CSS/DOM | swap via CSS/vars, out of scope for v1 |
## Architecture (two lanes + integration)
### Lane I — Asset runtime (`src/assets/`)
`assets/manifest.json` maps slot → `{url, offset, rotation, scale, options}`.
`AssetRegistry` (GLTFLoader + cache) resolves slots; **every construction site
falls back to today's procedural primitive when a slot is empty or fails to
load** — an empty manifest must produce today's game, byte-for-byte behavior.
Construction sites (createBlob, cannon barrel, machine parts, greybox scenery)
ask the registry instead of building directly, via a tiny `slotMesh(world,
'machine.boot', buildProcedural)` hook. Blob slot additionally: UV sanity check
(warn if overlapping/absent), PaintSkin adoption of the delivered material, and
optional AnimationMixer for a named idle clip. Local override layer:
manifest + GLB blobs can live in IndexedDB (set by the editor) and take
precedence — so custom assets are playtestable on the LIVE site with zero deploys.
### Lane J — Workshop editor (`/editor.html`, `src/editor/`)
A separate page sharing the game's modules. Three panels:
1. **Slots** — every slot with live thumbnail; click to select.
2. **Stage** — the actual game world (greybox + machines idling) rendering the
current assets; orbit camera; selected slot highlighted.
3. **Fit** — drag-drop a `.glb` from disk onto a slot → instant swap in the
stage; sliders/inputs for offset/rotation/scale; paintability report for
blob.body (UV coverage, tri count, material count); "Test drive" button
boots the REAL game in-page with overrides active.
Export: ⬇ `manifest.json` download + "Save locally" (IndexedDB override, with a
persistent "custom assets active — clear?" pill in the game). No server writes:
committing an asset pack = John drops the GLB in `assets/live/` + manifest in
the repo (documented in the panel's "ship it" help text).
### Integration (main session)
Wire registry into game.ts construction, ship `/blobbo/editor.html`, verify
(empty-manifest parity, toaster GLB in a scenery slot, blob swap with the farm
mesh incl. paint), deploy. Ship the 4 existing farm GLBs as the first pack.
## Blender/authoring conventions (put in the editor's help panel too)
- Y-up, -Z forward, meters; blob body ≈ 1.0u diameter, origin at CENTER;
props origin at ground-contact center. Single material preferred.
- Paintable blob: one UV island set, no overlaps, no mirrored UVs.
- Rigged: single armature, clips named `idle` (loop) and optionally `roll`;
keep ≤ ~40 bones (mobile later). GLB embedded textures ≤2048².
- Licensing per asset-pipeline skill: 🟢 web-ok only (farm-generated = ok).
## Out of scope v1 (candidate wave 4)
Course layout editing (move/add machines, puddles, boxes → course JSON),
animation retarget beyond idle clips, multi-pack management, shared packs.

View File

@ -0,0 +1,66 @@
# Lane I — Asset runtime (manifest, registry, slot hooks)
**Owns:** `src/assets/`, `assets/manifest.json`, `demos/lane-i.html`,
`src/demo/lane-i.ts`, PLUS surgical slot-hook edits inside `src/blob/createBlob.ts`,
`src/paint/cannon.ts` (barrel build only), `src/machine/parts.ts` (mesh builds
only), `src/course/greybox.ts` (scenery only). Those four are OTHER lanes' files:
your edits there must be strictly additive-visual — replace direct mesh
construction with a registry hook that DEFAULTS to the existing construction.
Never touch physics/colliders/logic in them, and never edit contracts/world/
game/main/package.json.
## Mission
Custom GLB assets drop into named slots without code changes; an empty manifest
produces today's game exactly. See docs/WORKSHOP-PLAN.md for the slot table —
implement every slot listed there except the UI rows.
## Deliverables
1. `src/assets/manifest.ts` — schema + loader:
```ts
interface SlotEntry { url: string; offset?: [n,n,n]; rotationDeg?: [n,n,n];
scale?: number | [n,n,n]; idleClip?: string }
type Manifest = Record<SlotId, SlotEntry>
```
Load order (later wins): `assets/manifest.json` (fetch, may 404 → {}) →
IndexedDB override (`blobbo-workshop` db: `manifest` + `blobs` stores; GLB
entries may be `idb:<key>` urls resolved to object URLs).
2. `src/assets/registry.ts``AssetRegistry`:
- `slotObject(slot, buildFallback: () => THREE.Object3D): THREE.Object3D`
synchronous placeholder pattern: returns a Group immediately containing the
fallback; if the manifest has the slot, async-load the GLB (GLTFLoader from
`three/examples/jsm` — already in the three package, no new deps), then
swap the group's child in place, applying offset/rotation/scale.
- Cache by url (clone via `SkeletonUtils.clone` for skinned, `.clone()` else).
- Load failure → keep fallback + one console.warn (never throw).
- `paintableInfo(object)` — for blob.body: find first Mesh/SkinnedMesh,
report { uvOk (uv attr exists & 0..1 bounded), triCount, materialCount }.
3. Blob slot specifics (in your createBlob hook):
- The delivered mesh's material `map` is replaced by PaintSkin's canvas
texture at integration (skin binds to whatever mesh is present); ensure
the hook exposes the swapped-in paintable mesh as `blob.mesh` (the skin
and buffs read it). If the GLB is a SkinnedMesh with `idleClip`, drive an
AnimationMixer in a fixed-step System (NOT onFrame — hidden-tab rule, see
telegraph.ts docstring for why).
- UV warning: if `paintableInfo.uvOk` is false, console.warn and keep the
procedural sphere instead (paint must never silently break).
4. `assets/manifest.json` — ship EMPTY `{}` (packs come later via integration).
5. Demo `demos/lane-i.html` + `src/demo/lane-i.ts`: a stage that builds every
slot twice — fallback vs a test manifest pointing at the four existing farm
GLBs (`assets/meshes/*.glb`) — side by side, with per-slot load status text
and the paintability report for blob.body.
## Acceptance
- `npm run build` green. With `{}` manifest: pixel-identical construction paths
(verify: no registry code path alters any existing geometry parameters —
the fallback builders are the ORIGINAL code moved, not rewritten).
- Demo loads all four farm GLBs into sensible slots (boot, bucket, toaster
scenery, blob body) with fit transforms committed in the demo's test manifest.
- Blob body swap: paint stamps visibly land on the swapped mesh in the demo
(drive a scripted splat at a known point).
- Failure paths: bad url → fallback + single warn; malformed manifest → `{}`.
- Slot-hook edits to the four foreign files are mesh-construction-only (diff
must show no collider/physics/logic lines changed).
## Contract friction
Report, never edit frozen files. If a slot needs data the hook can't reach,
document it for integration rather than widening interfaces.

View File

@ -0,0 +1,59 @@
# Lane J — Workshop editor (drop-in asset fitting + export)
**Owns:** `src/editor/`, `editor.html` (repo root, sibling of index.html),
`demos/lane-j.html`, `src/demo/lane-j.ts`.
**Never touches:** contracts, world, game, main, package.json, other lanes'
dirs. Depends on Lane I's `src/assets/` API (manifest.ts, registry.ts) — it
runs in a parallel worktree, so code against the INTERFACES specified in
lanes/LANE-I-asset-runtime.md §Deliverables 1-2 exactly; integration reconciles.
No new deps (GLTFLoader ships inside three).
## Mission
A browser page where John drags his own GLB files onto game-object slots, fits
them (offset/rotation/scale), sees them ON the real course, saves them locally
so the LIVE game uses them instantly, and exports a manifest for committing.
docs/WORKSHOP-PLAN.md is the product spec — read it first.
## Deliverables
1. `editor.html` + `src/editor/main.ts` — three-panel layout (CSS grid, no framework):
- **Slots panel (left):** every slot from the plan's table with status
(procedural / custom / override-active). Click selects.
- **Stage (center):** the greybox course + idle machine visuals (import the
real builders; physics optional — a static scene is fine, but reuse real
meshes so fits are truthful). OrbitControls (three/examples/jsm). Selected
slot's object gets an outline/pulse. Camera-focus button per slot.
- **Fit panel (right):** for the selected slot — drop zone ("drop .glb"),
numeric inputs + sliders for offset xyz / rotation xyz / uniform scale,
reset-to-procedural button, and for `blob.body` the paintability report
(uvOk / tris / materials) with a red warning banner when uvOk=false.
2. Drag-drop flow: File API → store blob in IndexedDB (`blobbo-workshop`/`blobs`),
manifest entry url `idb:<key>` → registry hot-swaps the stage object live.
3. **Save locally** — writes the working manifest to the IndexedDB override the
game reads (Lane I load order). **Export** — downloads `manifest.json`
(idb urls flagged with a "commit the .glb files to assets/live/ and change
urls" note in the download's `_readme` field). **Clear overrides** button.
4. **Test drive** — button that opens `/blobbo/` in a new tab (overrides are in
IndexedDB, so the live game picks them up — say so in the UI).
5. Ship-it help card: Blender conventions from the plan doc + "to make it
permanent: assets/live/ + manifest.json + redeploy".
6. Demo `demos/lane-j.html`: the editor booted with a scripted fake-drop of
`assets/meshes/prop-spring-boot.glb` onto `machine.boot` (fetch → same code
path as a real drop), asserting the stage object swapped and the exported
manifest round-trips.
## Acceptance
- `npm run build` green (editor.html added to YOUR OWN vite input? NO — you
can't edit vite.config.ts; note it as friction, integration adds the input).
- Demo's scripted drop swaps the stage mesh and the export round-trips
(parse the downloaded JSON blob in-page and deep-equal it).
- Fit controls visibly transform the object; values persist through slot
switches; reset restores procedural.
- blob.body drop with the farm blobbo GLB shows the paintability report.
- Overrides survive reload (IndexedDB), and Clear removes them.
- Zero console errors on an empty session (no drops, all procedural).
## UX bar
This is John's tool: label everything in plain words, no dev jargon on screen.
It should feel like a toy, not an IDE — big drop targets, instant feedback,
one obvious happy path (pick slot → drop file → nudge sliders → Save locally →
Test drive).