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

7.9 KiB
Raw Blame History

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: 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 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

  • 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 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 (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.