RECORDGOD/docs/DIG_FLIP_HANDOVER.md
type-two 916b26102c docs: DIG_FLIP_HANDOVER — port THRIFTGOD's evolved crate-riffle into /store
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:52:26 +10:00

104 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# HANDOVER: the record flip/riffle animation — port THRIFTGOD's dig back into RecordGod
**For:** the RecordGod agent. **Goal:** adopt the evolved crate-digging mechanic into the
3D store served at **robotmonster.party/store/** (this repo: `webstore/index.html` +
`webstore/dig.js`). **Written:** 2026-07-02 by the THRIFTGOD session. Self-contained —
you don't need that conversation.
## Lineage (read this first)
RecordGod's `webstore/dig.js` is the ORIGINAL crate-inspect scene. THRIFTGOD
(`/Users/johnking/Documents/OPSHOPGAME/web/dig.js`) forked it and evolved it through a
day of live play-testing. **The THRIFTGOD file is a superset of yours with the same
architecture** — same `createDig(renderer, opts)` factory, same scene/camera/update/open/
close contract, same procedural audio. The cleanest port is: diff the two files, take
everything, then re-wire the two integration points listed at the end.
`diff webstore/dig.js /Users/johnking/Documents/OPSHOPGAME/web/dig.js`
## The mechanics, and the numbers that make them feel right
All tuned by hand against real play. Change them at your peril; record them if you do.
1. **Riffle = a damped cursor over the stack.**
- `cursor` (float index) + `vel`; per frame: `vel *= Math.pow(0.0008, dt)`,
`cursor += vel * dt`, clamped to [0, n1].
- Wheel: `vel += e.deltaY * 0.012`. Drag: `cursor += dy * 0.02; vel = dy * 0.6`.
- When `Math.floor(cursor)` changes → play the **fwip** + load nearby covers + update
the title label.
2. **The flip is a hinge at the sleeve's bottom edge.**
- Each record = a Group at the bin floor; the sleeve mesh is offset `y = h/2` inside
it, so rotating the GROUP hinges at the bottom edge — this is the whole trick.
- Rest angle `BACK = 0.12` (sleeves lean back); flipped-forward max `MAXFLIP = 1.95`.
- Per frame, each record i targets:
`target = BACK + smoothstep(cursor i, 0.4, 1.2) * (MAXFLIP BACK)`
then eases: `angle += (target angle) * min(1, dt*12)`.
Records behind the cursor stand; records ahead lie flopped forward. The smoothstep
window (0.4…1.2) is what makes riffling feel like fingers walking sleeves.
3. **Real format sizes + packing by thickness** (THRIFTGOD addition):
- `FMT = { lp:[0.31,0.31,0.0035], cd:[0.142,0.125,0.010], dvd:[0.136,0.19,0.014],
vhs:[0.105,0.187,0.025], cass:[0.11,0.07,0.017] }` — [w, h, thickness] metres.
- Stack: `z -= thickness + 0.010` per sleeve. VHS riffles chunkier than vinyl for free.
- RecordGod is LP-only today, but keep the map — CDs are inevitable.
4. **Secondhand lean** (cheap, huge): per sleeve seeded jitter —
`rotation.z = ((i*2654435761>>>0)%10050)/1800`, `position.x = ((i*40503>>>0)%10050)/12000`.
No more parade-ground vinyl.
5. **The crate is drawn around the stack** (THRIFTGOD addition): floor/left/right/back
walls + a LOWER front lip (`H*0.55`) the sleeves flip over; sized from max sleeve dims
+ stack depth; wood texture (any wood jpg with RepeatWrapping ×2). See `buildCrate()`.
6. **Covers trickle-load; near-cursor loads win.**
- Priority window on cursor move: indices `c10 … c+15` load immediately.
- Background: `fillQueue()` loads ONE unloaded cover every 120ms until the crate is
full (clear the timer in `close()`). Never load all covers eagerly; never texture
more than the window synchronously. `tx.colorSpace = THREE.SRGBColorSpace` always.
7. **Pull-to-inspect presents at arm's length ON THE VIEW AXIS** (final tuning after two
bad iterations — first it cropped off-screen, then it became IMAX):
- Dig camera: `position (0, 0.5, 0.92)`, `lookAt (0, 0.12, 0.2)`, FOV 45.
- Pull target FOR THAT CAMERA: `position → (0.1, 0.23 h/2, 0.12)` (h = sleeve
height; the 0.1 x keeps it clear of a right-side info panel), and
`rotation.x → 0.33` so the cover faces the lens.
- **If your camera differs, derive it**: `target = cam.pos + viewDir * 0.85`, then
`y = h/2`, and tilt to face the camera. 0.85m gives an LP ~21° of a 45° FOV.
- Lerp position `min(1, dt*5)`, rotation `min(1, dt*6)`.
- `unpull()` restores `position.set(0, 0, homeZ)`; the angle system reclaims rotation.
- Click-vs-drag: pull only fires if total pointer movement `< 4px`.
8. **After a sale, re-pack the stack**: remove the record, then walk the survivors
re-assigning `homeZ` with the same thickness accumulation. Don't leave a gap.
9. **Procedural audio, no asset files** (already in your ancestor, keep it):
- *fwip*: 50ms white-noise burst → bandpass 2100Hz Q0.7 → gain 0.22.
- *thunk* (pull): sine 105Hz, 5ms attack to 0.5, exp decay over ~0.28s.
- Lazily create AudioContext on first use (autoplay policies).
10. **Pointer-lock gotcha** (if your store uses PointerLockControls): Chrome refuses
re-lock within ~1.3s of Esc. Never call `controls.lock()` programmatically after
closing a panel — wrap it: request, `.catch()` → show a click-to-resume overlay
(a fresh user gesture always succeeds). THRIFTGOD calls this `safeLock()` in
`web/index.html`.
## Integration points (the only THRIFTGOD-specific bits to rewire)
- **Data in:** THRIFTGOD uses `createDig(renderer, { onClose, getItems, onBought })` with
`open(kind)`; your ancestor used `getRecords` + `open(crateId)` fetching
`/virtual/crate/{id}/records`. Keep YOUR fetch, adopt their buildStack. Records need
`{ title, artist, thumb, price, condition, fmt? }` (fmt defaults to `lp`).
- **Buy action:** THRIFTGOD's pull-panel button POSTs `/api/take` (its basket system).
Replace with RecordGod's add-to-cart. Everything else in `pull()`/`unpull()` ports as-is.
- **Images:** THRIFTGOD proxies external covers via `/img?u=`. RecordGod serves its own
webp at `/store/assets/` — use your `thumb` URLs directly, no proxy needed.
## Definition of done
Riffle with wheel AND drag feels weighted; sleeves lean individually; the crate is
visible around the stack; covers fill in behind you; pulling any format presents fully
in frame, facing the camera, beside (not under) the info panel; buying closes the gap
in the stack; Esc steps back out cleanly twice (pull → crate → store) with no
pointer-lock console errors.