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>
3.9 KiB
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
src/assets/manifest.ts— schema + loader:
Load order (later wins):interface SlotEntry { url: string; offset?: [n,n,n]; rotationDeg?: [n,n,n]; scale?: number | [n,n,n]; idleClip?: string } type Manifest = Record<SlotId, SlotEntry>assets/manifest.json(fetch, may 404 → {}) → IndexedDB override (blobbo-workshopdb:manifest+blobsstores; GLB entries may beidb:<key>urls resolved to object URLs).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 fromthree/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.clonefor 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 }.
- Blob slot specifics (in your createBlob hook):
- The delivered mesh's material
mapis replaced by PaintSkin's canvas texture at integration (skin binds to whatever mesh is present); ensure the hook exposes the swapped-in paintable mesh asblob.mesh(the skin and buffs read it). If the GLB is a SkinnedMesh withidleClip, drive an AnimationMixer in a fixed-step System (NOT onFrame — hidden-tab rule, see telegraph.ts docstring for why). - UV warning: if
paintableInfo.uvOkis false, console.warn and keep the procedural sphere instead (paint must never silently break).
- The delivered mesh's material
assets/manifest.json— ship EMPTY{}(packs come later via integration).- 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 buildgreen. 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.