# Social & the Reset Ritual — Handoff **Brief:** [docs/briefs/SOCIAL_RESET.md](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](../src/net/avatarLook.ts)** (new) — the shared vocabulary: `hue` 0–11, `gear` 0–4 (`phones|cap|beanie|crown|none`), `face` 0–3 (`dots|shades|visor|happy`). All bounded ints — no free-form content, nothing to moderate. `validLook()` mirrors the relay's clamp; `bodyHSL`/ `bodyCss`/`paintFace` are 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](../src/net/Avatars.ts)** renders the look, plus arms (needed for emotes). `updateAvatar()` rebuilds that peer's meshes (they're tiny). - **[Menu.ts](../src/ui/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 - `Z` wave · `X` nod (on the beat; free-runs at 118 BPM when the booth is silent; auto-off after 16 bars) · `C` point · `V` airhorn. Pointer-locked only, so no hotbar collision. - **Airhorn** ([sfx.ts](../src/audio/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](../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) 1. **`src/interact/**` isn't in this brief's ownership list**, but the ritual is defined as a mining behaviour and the break time lives in `interaction.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. 2. **The camera-nudge self-view** was dropped (needs a Lane B API) — see above. 3. **Pre-existing, not introduced:** a peer who never *moves* is invisible to a late joiner, because `hello` carries no positions and avatars stay hidden until the first `pos`. Worth a one-line fix upstream (send a pos on join, or include positions in `hello`) 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 | ✅ `!!!` → `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.