From d62bcfea8257b545f8ca44167c22291ae7474ebe Mon Sep 17 00:00:00 2001 From: Keith Avery Date: Sat, 25 Apr 2026 01:42:53 -0400 Subject: [PATCH 1/2] fix(mp): read character_slot from core.name so PLAYER_SEAT actually fires The seat handshake silently no-op'd in multiplayer playtests. Server emits character.model_dump(mode="json") whose Pydantic shape nests the name under `core.name` (Character.core: CreatureCore in sidequest-server/sidequest/game/character.py:69-94). The chargen-complete handler in App.tsx was reading the flat `charData.name` / `charData.character_name`, both undefined for the current emission, so the `if (charNameForSeat && sendRef.current)` guard fell through and PLAYER_SEAT was never sent. Server-side never logged `session.player_seated`, no `mp_seat_span` event, no SEAT_CONFIRMED broadcast. Read `core.name` as the primary lookup; keep flat names as fallback for back-compat with any alternative emission paths. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/App.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index 65f0b5c..ad54d26 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -475,7 +475,15 @@ function AppInner() { // names; mutant_wasteland uses class+name). The DB primary key // for the seat is (slug, player_id), so the slot is mostly // descriptive metadata that flows back via SEAT_CONFIRMED. + // + // The server emits `character.model_dump()` which nests the + // name under `core.name` (Python pydantic model). The flat + // `name` / `character_name` fallbacks are kept for any + // historical or alternative emission paths but the canonical + // location is `core.name`. + const charCore = charData?.core as Record | undefined; const charNameForSeat = + (charCore?.name as string | undefined) ?? (charData?.name as string | undefined) ?? (charData?.character_name as string | undefined); if (charNameForSeat && sendRef.current) { From 2e2b7fcc1d37f3bbc3442ba94e5beb23d752e0d6 Mon Sep 17 00:00:00 2001 From: Keith Avery Date: Sat, 25 Apr 2026 01:43:08 -0400 Subject: [PATCH 2/2] feat(ui): heartbeat dot in chargen-commit loading view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Playtest 2026-04-24 flagged the all-text spinner ("Considering your words..." then "Waiting for the narrator...") as indistinguishable from a crash — a black viewport with one line of italic text and no other motion is read as a freeze, especially during the 30-90s chargen-commit → opening-narration window. OQ-1's MultiplayerTurnBanner already established the heartbeat idiom for the in-game state. Mirror it for chargen: a w-2 h-2 emerald animate-pulse dot next to the loading text. Same shape, same testid prefix, distinct id (`chargen-heartbeat-dot`) so the two views don't collide in tests. CharacterCreation.loading.test.tsx — 2 cases: 1. default copy renders the heartbeat dot 2. genre-pack loading_text override still renders the heartbeat Co-Authored-By: Claude Opus 4.7 (1M context) --- .../CharacterCreation/CharacterCreation.tsx | 11 ++++- .../CharacterCreation.loading.test.tsx | 40 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/components/CharacterCreation/__tests__/CharacterCreation.loading.test.tsx diff --git a/src/components/CharacterCreation/CharacterCreation.tsx b/src/components/CharacterCreation/CharacterCreation.tsx index cb3ddeb..4fdfae3 100644 --- a/src/components/CharacterCreation/CharacterCreation.tsx +++ b/src/components/CharacterCreation/CharacterCreation.tsx @@ -59,10 +59,19 @@ export function CharacterCreation({ scene, loading, onRespond }: CharacterCreati // turn. "Waiting for the narrator..." is true regardless of which // chargen step we're between. Genre packs can override via // ``scene.loading_text``. + // + // The heartbeat dot is the "system is alive" cue — playtest 2026-04-24 + // flagged the all-text spinner as indistinguishable from a crash. Pulse + // matches the in-game MultiplayerTurnBanner idiom (emerald, w-2 h-2). return (
+ className="flex items-center justify-center gap-2 min-h-[200px]"> +