From 5802e1df050a1c956c7ef5f2325c30bd3b23ca93 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 16 Aug 2026 16:00:51 -0400 Subject: [PATCH] Table talk keeps its place in the chronicle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live chat interleaved correctly, but a reload sank every past message to the bottom: the rejoin sent the whole game history first and the chat history after. Talk is now a tableTalk event in the room's event stream at the moment it was said — the room file already preserved the interleaving, since chat lines append between command lines — so rejoining players read the banter between the battle lines where it happened. The separate chatHistory message retires; unread counting rides the event stream instead. Co-Authored-By: Claude Fable 5 --- .../console-2026-08-16T19-34-16-116Z.log | 45 +++++++++++++++++++ packages/engine/src/game.ts | 1 + packages/server/src/index.ts | 4 -- packages/server/src/rooms.ts | 5 +++ packages/web/src/net.svelte.ts | 21 ++++----- 5 files changed, 60 insertions(+), 16 deletions(-) diff --git a/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log b/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log index 238f83f..70373cd 100644 --- a/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log +++ b/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log @@ -330,3 +330,48 @@ [ 1383416ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 [ 1388071ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 [ 1391775ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1395068ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1401362ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1405927ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1409973ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1415788ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1419993ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1422568ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1427897ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1431247ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1436017ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1440676ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1444279ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1447985ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1453812ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1459007ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1464539ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1469651ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1472486ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1478828ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1483427ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1488790ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1492253ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1496471ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1500655ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1504766ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1507334ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1513726ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1517718ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1520603ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1523915ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1529894ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1535783ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1539666ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1543362ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1546661ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1550202ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1553917ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1559517ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1562654ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1566220ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1570798ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1573997ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1577376ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1583494ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1588863ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index f27b095..79519c5 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -501,6 +501,7 @@ export type GameEvent = | { type: "moveBumped"; player: PlayerId; direction: Side } | { type: "attackMisdirected"; attacker: PlayerId; intended: PlayerId; rolledDirection: Side; newTarget: PlayerId | null } | { type: "dieRolled"; player: PlayerId | null; roll: number; purpose: string } + | { type: "tableTalk"; player: PlayerId; text: string } | { type: "retreatedInHorror"; player: PlayerId; from: Cell; to: Cell } | { type: "illusionWallCreated"; caster: PlayerId; edge: { cell: Cell; side: Side } } | { type: "illusionTested"; player: PlayerId; edge: string; result: "believes" | "seesThrough" } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d70f0d4..4b9787e 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -19,7 +19,6 @@ // {type:"events", events} redacted for this recipient // {type:"state", view, seq} redacted full view (after every change) // {type:"chat", player, text, at} one line of table talk -// {type:"chatHistory", messages} the room's talk so far, on join // {type:"transferCode"|"transferClaimed"|"catchUp"|"games"|"stats"} // {type:"error", message} @@ -250,9 +249,6 @@ wss.on("connection", (socket) => { if (room.state) { send(socket, { type: "events", events: redactFor(room.events, name) }); } - if (room.chat.length > 0) { - send(socket, { type: "chatHistory", messages: room.chat.slice(-100) }); - } broadcastRoomState(room); break; } diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 5086a0f..7fd202c 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -223,6 +223,9 @@ export function addChat(room: Room, playerId: PlayerId, rawText: string): { text const at = new Date().toISOString(); room.chat.push({ player: playerId, text, at }); if (room.chat.length > CHAT_KEEP) room.chat.splice(0, room.chat.length - CHAT_KEEP); + // Talk takes its place in the chronicle where it was said, so rejoining + // players see it interwoven with the action, not sunk to the bottom. + room.events.push({ type: "tableTalk", player: playerId, text }); appendLine(room.id, { kind: "chat", player: playerId, text, at }); return { text, at }; } @@ -435,7 +438,9 @@ export function loadPersistedRooms(): void { const r = startInMemory(room, line.expansion, line.colors, line.deckRev); if ("error" in r) throw new Error(`replay start failed: ${r.error}`); } else if (line.kind === "chat") { + // File order preserves the interleaving with commands. room.chat.push({ player: line.player, text: line.text, at: line.at }); + room.events.push({ type: "tableTalk", player: line.player, text: line.text }); } else if (line.kind === "command") { if (!room.state) throw new Error("command before start in log"); const result = applyCommand(room.state, line.playerId, line.command as Command); diff --git a/packages/web/src/net.svelte.ts b/packages/web/src/net.svelte.ts index 506b1f7..7639d87 100644 --- a/packages/web/src/net.svelte.ts +++ b/packages/web/src/net.svelte.ts @@ -145,6 +145,7 @@ export function humanize(e: GameEvent): string | null { case "objectPickedUp": return `${e.player} picks up the ${cardDef(e.card.cardId).name} — actions over.`; case "wardSet": return e.armed ? "Your ward is set — the next thief bleeds." : "Your ward stands down."; case "chaosShielded": return `${e.player} raises a FULL SHIELD and sits out the chaos.`; + case "tableTalk": return `\u{1F4AC} ${e.player}: ${e.text}`; case "dieRolled": return `\u{1F3B2} ${e.player ?? "The maze"} rolls a ${e.roll} — ${e.purpose}.`; case "pushed": return e.player ? `${e.by} shoves ${e.player} down the corridor!` @@ -320,12 +321,19 @@ class Net { case "catchUp": this.catchUp = msg.steps; break; - case "events": + case "events": { + let talk = 0; for (const e of msg.events as GameEvent[]) { + if (e.type === "tableTalk") talk++; const line = humanize(e); if (line) this.log = [...this.log, line]; } + if (talk > 0) { + this.chatCount += talk; + if (this.roomId) this.markChatSeen(); + } break; + } case "transferCode": this.transferCode = { code: msg.code, expiresAt: msg.expiresAt }; break; @@ -342,17 +350,6 @@ class Net { if (this.roomId) this.markChatSeen(); break; } - case "chatHistory": { - this.log = [ - ...this.log, - ...(msg.messages as { player: string; text: string }[]).map( - (m) => `\u{1F4AC} ${m.player}: ${m.text}`, - ), - ]; - this.chatCount = msg.messages.length; - if (this.roomId) this.markChatSeen(); - break; - } case "stats": { this.stats = msg.stats; break;