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

6.1 KiB
Raw Blame History

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.