Built by the Opus session per docs/briefs/SOCIAL_RESET.md; integrated and verified by Fable (typecheck clean, relay validation reviewed, 3D avatar gear/face pixel-confirmed live — the one item the build session couldn't capture in its frozen preview pane). - Avatars: hue/gear/face enums in src/net/avatarLook.ts shared by the 3D build and the Menu AVATAR tab's 2D mirror; live sync with client-side coalescing (two-tab testing caught the relay rate limit permanently desyncing rapid editors - trailing send fixes convergence). - Emotes Z/X/C/V: wave, beat-synced nod, point, rate-limited airhorn (detuned saws + confetti), server + client caps. - Reset ritual: after a win the fuse takes a deliberate 5s hold (reuses the workshop radial meter as the warning dial) -> server-authoritative reset with 10-min cooldown; music brakes to silence, lights die over 3s, workshop resets to wires-off with a SERVER-generated random weight notch (both tabs land identical), player builds survive, quest replayable. - Roster: hold Tab for who's-in-the-booth with hue swatches. - interact/ addition (flagged friction): additive setBreakGuard hook so interaction.ts never learns what a fuse is. - launch.json: main dev config uses autoPort (5173 collisions with parallel sessions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.9 KiB
Social & the Reset Ritual — Handoff
Brief: docs/briefs/SOCIAL_RESET.md
Status: ✅ All four features implemented, verified live in two tabs against a
real relay. npm run typecheck clean, zero console errors across the session.
Not deployed — the integrator ships it (the brief says don't touch deploy).
What shipped
1. Avatar customization
- src/net/avatarLook.ts (new) — the shared
vocabulary:
hue0–11,gear0–4 (phones|cap|beanie|crown|none),face0–3 (dots|shades|visor|happy). All bounded ints — no free-form content, nothing to moderate.validLook()mirrors the relay's clamp;bodyHSL/bodyCss/paintFaceare shared by the 3D avatar AND the 2D mirror so they cannot drift apart. hue: 0= "auto" — keeps the original golden-ratio per-peer hue, so every existing avatar looks exactly as it did before.- Avatars.ts renders the look, plus arms (needed
for emotes).
updateAvatar()rebuilds that peer's meshes (they're tiny). - Menu.ts — third tab AVATAR: swatch/chip rows and a 2D canvas mirror ("you're first person — so here's the mirror"). Applies live, persists in the existing Settings object.
2. Emotes
Zwave ·Xnod (on the beat; free-runs at 118 BPM when the booth is silent; auto-off after 16 bars) ·Cpoint ·Vairhorn. Pointer-locked only, so no hotbar collision.- Airhorn (sfx.ts
airhorn()): three detuned saws → resonant bandpass → tanh grit, with the signature pitch droop. Positional at the emoter; confetti puff from FxSystem. - Rate limited client AND server (1 per 4 s per peer, drop-never-queue).
- Self-view: first person can't see its own wave, so you get the sound plus a
small toast. Deviation: the brief asked for a "tiny camera nudge" — that
needs a Lane B API on
PlayerController(not in this brief's ownership), so the toast stands in. Trivial to swap if Lane B adds a nudge.
3. The Reset Ritual
- Fuse voxel takes a deliberate 5 s hold once the booth is won, with the workshop's radial meter reused as the "KEEP MINING TO KILL THE MIX — EVERYONE STARTS OVER" dial.
- Server-authoritative: validates win + 10-min anti-grief cooldown,
clears repairs, stops platters, sets the workshop reset pose
(assembled-but-undone: cart seated + square, all four wires popped off,
screws 0.5, weight a random notch 3–6), keeps all block edits, persists
winAt, broadcasts to everyone including the breaker. - Client:
Quest.reset(by)→quest:reset→ music brakes to silence (transport rate → 0, lowpass collapse, contactor thunk + dying mains hum — a power-down, not a mute), FX eases emissive back to 0.35 over ~3 s, tracker clears, platters stop, and the blown fuse reappears in the box. - Cooldown rejection shows "the fuse is still hot — give it a minute" and vetoes the break, so you don't lose the fuse to a refused reset.
4. Who's here
- src/ui/Roster.ts (new) — hold Tab: translucent list, you first, each with their avatar hue as a swatch. HTML-escaped (names are user-controlled even though the relay sanitizes them).
Files touched
| file | what |
|---|---|
server/relay.mjs |
protocol v2: avatar/emote/reset, rate limits, winAt, reset pose |
src/core/events.ts |
+quest:reset, +player:emote (additive, primitives only) |
src/machines/quest.ts |
+Quest.reset(by) (idempotent) |
src/net/avatarLook.ts |
new — shared look vocabulary |
src/net/Avatars.ts |
look rendering, arms, emote animation, positionOf, dispose |
src/net/NetClient.ts |
avatar/emote/reset wiring, roster, avatar coalescing |
src/ui/Menu.ts |
AVATAR tab + mirror, avatar setting, help text (emotes, Tab, fuse) |
src/ui/Roster.ts |
new — who's-here |
src/audio/sfx.ts |
+airhorn, +powerDown |
src/audio/AudioEngine.ts |
runResetAudio() brake-to-silence; emote/reset bus wiring |
src/fx/FxSystem.ts |
reset boost ease-down + spark cough; airhorn confetti |
src/interact/interaction.ts |
+setBreakGuard() (additive hook — see friction) |
src/main.ts |
emote keys, roster, avatar live-apply, fuse guard, blown-fuse restore |
docs/INTEGRATION_NOTES.md |
protocol v2 section |
Merge points: src/main.ts, src/ui/Menu.ts — both kept additive.
Contract friction (for the integrator)
src/interact/**isn't in this brief's ownership list, but the ritual is defined as a mining behaviour and the break time lives ininteraction.ts:179. I added the smallest possible additive hook (setBreakGuard) rather than special-casing "fuse" inside interact. With no guard installed, mining is unchanged. Flagging rather than assuming.- The camera-nudge self-view was dropped (needs a Lane B API) — see above.
- Pre-existing, not introduced: a peer who never moves is invisible to a
late joiner, because
hellocarries no positions and avatars stay hidden until the firstpos. Worth a one-line fix upstream (send a pos on join, or include positions inhello) if it ever bites.
Verification (two tabs, real relay, real browser)
| check | result |
|---|---|
| Avatar change → peer sees it, no reload | ✅ propagates in well under 1 s |
| Rapid hue→gear→face | ✅ converges to the full look (bug found + fixed, below) |
| Garbage avatar from console | ✅ {hue:999,gear:-3,face:'x'} → {0,0,0} |
| Name fuzz | ✅ <script>bad</script>!!! → scriptbadscript |
| Emote spam (2 valid airhorns + bad enum) | ✅ exactly one landed remotely |
| Relay fuzz (bad types/enums/unknown msg) | ✅ all dropped, socket stayed alive |
| Reset before win | ✅ rejected |
| Reset immediately after win | ✅ rejected (cooldown) |
| Full reset (cooldown expired) | ✅ both tabs: won→false, 5→0 repairs, blown fuse back in box, platters stopped, subtitle "⚡ dj groove72 BLEW THE FUSE" |
| Workshop reset pose | ✅ identical on both tabs (weightNotch: 4 — proves the server-authoritative pose) |
| Player builds survive the reset | ✅ placed block still present |
| Replayable to a second win | ✅ both tabs |
| Late joiner post-reset | ✅ reloaded tab got the won→reset state from hello |
winAt persistence + old-file load |
✅ saved, rewound, reloaded (loaded state: 0 edits, 5 repairs) |
| Tab roster + hue swatches | ✅ screenshotted |
| AVATAR tab + live mirror | ✅ screenshotted (indigo + crown + shades) |
| typecheck / console errors | ✅ clean / zero, both tabs |
The bug two-tab testing caught
Clicking hue→gear→face quickly (the normal way to use the editor) left peers on
a stale avatar permanently: the relay's 1-per-2 s limit dropped messages 2
and 3, and because each message carries the whole look rather than a delta,
the peer never converged. NetClient.setAvatar now coalesces — it always
schedules the latest look for when the server's window reopens. Re-tested: rapid
lime+beanie+happy converges to {4,2,3}.
Not visually confirmed
The 3D peer avatar's gear/face rendering could not be pixel-confirmed: the
WebGL canvas in this preview pane stops repainting (verified — the frame is
byte-identical with the camera slammed 240 units up looking straight down; DOM
overlays still update). What I did verify programmatically: the peer's scene
graph is exactly right (tag + body + head + 2 arm groups + cap + bobble for a
beanie), visible: true, inside the camera frustum, zero errors — and the 2D
mirror, which shares the same paintFace/bodyCss code, renders correctly.
Worth one human glance in a real browser before shipping.
Out of scope (untouched, per brief)
Text chat (deliberately never), voice, friend lists, accounts, mobile input, deploy scripts.