The three that stopped a pack shipping: - a mesh-less GLB (armature-only export) fired onSwap anyway, hiding the primitive and adding nothing — an invisible prop with a live collider. Mesh count is now checked BEFORE the scene is touched, in one guard that covers every consumer including slotObject/blob.face. - the fulfilment path had no try/catch, so a throw became an unhandled rejection. Split in two: a build failure bails out before onSwap can hide anything; an onSwap failure keeps the replacement parented, because consumers hide their primitive on onSwap's first line and removing the fit node there would manufacture the one forbidden state. - preload() awaited every url forever. Each is now raced against a 10s deadline with allSettled semantics: a stalled host costs one warning and a fallback prop, not a game that never boots. Also: - instantiate/instanceSync never throw — createBlob's call site is frozen and unguarded, so a throw there is a black screen. - ghost and blob share fitBodyToRadius(); a borrowed blob.body was rendering the ghost 2.17x oversized with the farm mesh. - skinned blob.body is rejected loudly instead of silently half-working; the idle-clip mixer is gone (it animated an orphaned skeleton in the fixed step). - paintableInfo counts UV islands: blobbo-base.glb passes every other check and still paints wrong at 1140 charts. Warning, not rejection. - slots.ts gains cannon.base, course.finish, course.tunnel, course.tramp, all hooked except tramp (built in frozen game.ts). - manifest ignores _-prefixed metadata keys instead of calling them typos. Empty-manifest parity verified byte-for-byte: scripts/sacred-parity.check.ts fingerprints the built scene and reports 49df4f20 on main and on this branch.
76 lines
4.4 KiB
Markdown
76 lines
4.4 KiB
Markdown
# 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).
|
|
|
|
**NOT BUILT — SkinnedMesh + `idleClip` was authored here and does not work.**
|
|
`createBlob` re-parents the body MESH alone into its own group, leaving the
|
|
GLB's bones outside the scene graph with no `matrixWorld` update, so the
|
|
clip animates nothing while burning fixed-step CPU; and the body's geometry
|
|
is re-centred/re-scaled to the collider radius, which invalidates the
|
|
skeleton's bind matrices. `paintableInfo` therefore REJECTS a skinned
|
|
`blob.body` (`ok` requires `!skinned`), `idleClip` is ignored with a warning
|
|
and no AnimationMixer is created. Making it real means keeping the GLB root
|
|
in the scene graph and fitting via a parent node instead of baking into the
|
|
geometry — a design change, not a bug fix.
|
|
- 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.
|