TURNCRAFT/docs/SOCIAL_RESET_handoff.md
jing 05c2765bd8 SOCIAL_RESET: avatars, emotes, the reset ritual, who's-here roster
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>
2026-07-17 02:01:42 +10:00

144 lines
7.9 KiB
Markdown
Raw Permalink 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.

# 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` 011, `gear` 04 (`phones|cap|beanie|crown|none`), `face`
03 (`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 36), **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.