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>
144 lines
7.9 KiB
Markdown
144 lines
7.9 KiB
Markdown
# 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 | ✅ `<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.
|