diff --git a/docs/WORKSHOP-PLAN.md b/docs/WORKSHOP-PLAN.md new file mode 100644 index 0000000..2b1725e --- /dev/null +++ b/docs/WORKSHOP-PLAN.md @@ -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. diff --git a/lanes/LANE-I-asset-runtime.md b/lanes/LANE-I-asset-runtime.md new file mode 100644 index 0000000..cbe4fe6 --- /dev/null +++ b/lanes/LANE-I-asset-runtime.md @@ -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 + ``` + Load order (later wins): `assets/manifest.json` (fetch, may 404 → {}) → + IndexedDB override (`blobbo-workshop` db: `manifest` + `blobs` stores; GLB + entries may be `idb:` 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. diff --git a/lanes/LANE-J-workshop-editor.md b/lanes/LANE-J-workshop-editor.md new file mode 100644 index 0000000..07a6b49 --- /dev/null +++ b/lanes/LANE-J-workshop-editor.md @@ -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:` → 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).