From df52e9b53a042ff127ad698d864c93d8743f4452 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 16 Aug 2026 14:24:22 -0400 Subject: [PATCH] Table talk: chat woven into the chronicle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A say-box under the chronicle sends table talk to the room; messages land in the event log itself, styled as parchment asides — banter between the battle lines, the way it happens at a real table. Talk is seat-authenticated, stripped and capped at 300 chars, rate-limited by the existing bucket, persisted as chat lines in the room file (the game replay ignores them; the room keeps its last 500), and replayed to anyone joining. Async games get unread badges in the lobby ledger (💬3), cleared by watching the table, and catch-up replays speak the lines at the step where they were said. Public to the whole room, no whispers — and no chat in hotseat, where the table talks for itself. Co-Authored-By: Claude Fable 5 --- .../console-2026-08-16T17-53-12-656Z.log | 101 ++++++++++++++++++ packages/server/src/index.ts | 15 +++ packages/server/src/rooms.ts | 30 ++++++ packages/server/src/store.ts | 9 +- packages/web/src/App.svelte | 39 ++++++- packages/web/src/Replay.svelte | 8 +- packages/web/src/net.svelte.ts | 47 +++++++- 7 files changed, 242 insertions(+), 7 deletions(-) diff --git a/.playwright-mcp/console-2026-08-16T17-53-12-656Z.log b/.playwright-mcp/console-2026-08-16T17-53-12-656Z.log index d0a8775..36ccf2c 100644 --- a/.playwright-mcp/console-2026-08-16T17-53-12-656Z.log +++ b/.playwright-mcp/console-2026-08-16T17-53-12-656Z.log @@ -344,3 +344,104 @@ [ 1452368ms] [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-CAp618mF.js:1 [ 1458783ms] [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-CAp618mF.js:1 [ 1462262ms] [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-CAp618mF.js:1 +[ 1467373ms] [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-CAp618mF.js:1 +[ 1470884ms] [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-CAp618mF.js:1 +[ 1474064ms] [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-CAp618mF.js:1 +[ 1477555ms] [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-CAp618mF.js:1 +[ 1480103ms] [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-CAp618mF.js:1 +[ 1483605ms] [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-CAp618mF.js:1 +[ 1488167ms] [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-CAp618mF.js:1 +[ 1492812ms] [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-CAp618mF.js:1 +[ 1497873ms] [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-CAp618mF.js:1 +[ 1501672ms] [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-CAp618mF.js:1 +[ 1506179ms] [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-CAp618mF.js:1 +[ 1509169ms] [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-CAp618mF.js:1 +[ 1511741ms] [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-CAp618mF.js:1 +[ 1517646ms] [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-CAp618mF.js:1 +[ 1520993ms] [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-CAp618mF.js:1 +[ 1525701ms] [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-CAp618mF.js:1 +[ 1531664ms] [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-CAp618mF.js:1 +[ 1534270ms] [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-CAp618mF.js:1 +[ 1540398ms] [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-CAp618mF.js:1 +[ 1545631ms] [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-CAp618mF.js:1 +[ 1551430ms] [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-CAp618mF.js:1 +[ 1555883ms] [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-CAp618mF.js:1 +[ 1561789ms] [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-CAp618mF.js:1 +[ 1565419ms] [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-CAp618mF.js:1 +[ 1571348ms] [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-CAp618mF.js:1 +[ 1577540ms] [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-CAp618mF.js:1 +[ 1580171ms] [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-CAp618mF.js:1 +[ 1583175ms] [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-CAp618mF.js:1 +[ 1587237ms] [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-CAp618mF.js:1 +[ 1590726ms] [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-CAp618mF.js:1 +[ 1595865ms] [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-CAp618mF.js:1 +[ 1601873ms] [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-CAp618mF.js:1 +[ 1603377ms] [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-CAp618mF.js:1 +[ 1604882ms] [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-CAp618mF.js:1 +[ 1606385ms] [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-CAp618mF.js:1 +[ 1607886ms] [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-CAp618mF.js:1 +[ 1609391ms] [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-CAp618mF.js:1 +[ 1610897ms] [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-CAp618mF.js:1 +[ 1612401ms] [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-CAp618mF.js:1 +[ 1613918ms] [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-CAp618mF.js:1 +[ 1615437ms] [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-CAp618mF.js:1 +[ 1617018ms] [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-CAp618mF.js:1 +[ 1618600ms] [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-CAp618mF.js:1 +[ 1620186ms] [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-CAp618mF.js:1 +[ 1622078ms] [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-CAp618mF.js:1 +[ 1624517ms] [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-CAp618mF.js:1 +[ 1626966ms] [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-CAp618mF.js:1 +[ 1630727ms] [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-CAp618mF.js:1 +[ 1636685ms] [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-CAp618mF.js:1 +[ 1641233ms] [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-CAp618mF.js:1 +[ 1646675ms] [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-CAp618mF.js:1 +[ 1650627ms] [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-CAp618mF.js:1 +[ 1656147ms] [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-CAp618mF.js:1 +[ 1661012ms] [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-CAp618mF.js:1 +[ 1664297ms] [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-CAp618mF.js:1 +[ 1669486ms] [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-CAp618mF.js:1 +[ 1675362ms] [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-CAp618mF.js:1 +[ 1678087ms] [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-CAp618mF.js:1 +[ 1681784ms] [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-CAp618mF.js:1 +[ 1684714ms] [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-CAp618mF.js:1 +[ 1690197ms] [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-CAp618mF.js:1 +[ 1694231ms] [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-CAp618mF.js:1 +[ 1696742ms] [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-CAp618mF.js:1 +[ 1702523ms] [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-CAp618mF.js:1 +[ 1707245ms] [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-CAp618mF.js:1 +[ 1710064ms] [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-CAp618mF.js:1 +[ 1712824ms] [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-CAp618mF.js:1 +[ 1716844ms] [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-CAp618mF.js:1 +[ 1721241ms] [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-CAp618mF.js:1 +[ 1727490ms] [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-CAp618mF.js:1 +[ 1731235ms] [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-CAp618mF.js:1 +[ 1737620ms] [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-CAp618mF.js:1 +[ 1740738ms] [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-CAp618mF.js:1 +[ 1745610ms] [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-CAp618mF.js:1 +[ 1749658ms] [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-CAp618mF.js:1 +[ 1752580ms] [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-CAp618mF.js:1 +[ 1757381ms] [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-CAp618mF.js:1 +[ 1763004ms] [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-CAp618mF.js:1 +[ 1766308ms] [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-CAp618mF.js:1 +[ 1769306ms] [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-CAp618mF.js:1 +[ 1774715ms] [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-CAp618mF.js:1 +[ 1780095ms] [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-CAp618mF.js:1 +[ 1784290ms] [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-CAp618mF.js:1 +[ 1788290ms] [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-CAp618mF.js:1 +[ 1794315ms] [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-CAp618mF.js:1 +[ 1798954ms] [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-CAp618mF.js:1 +[ 1804568ms] [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-CAp618mF.js:1 +[ 1810874ms] [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-CAp618mF.js:1 +[ 1814383ms] [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-CAp618mF.js:1 +[ 1818248ms] [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-CAp618mF.js:1 +[ 1823554ms] [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-CAp618mF.js:1 +[ 1828717ms] [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-CAp618mF.js:1 +[ 1831520ms] [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-CAp618mF.js:1 +[ 1837969ms] [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-CAp618mF.js:1 +[ 1841753ms] [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-CAp618mF.js:1 +[ 1846062ms] [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-CAp618mF.js:1 +[ 1848849ms] [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-CAp618mF.js:1 +[ 1854446ms] [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-CAp618mF.js:1 +[ 1857424ms] [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-CAp618mF.js:1 +[ 1862532ms] [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-CAp618mF.js:1 +[ 1865457ms] [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-CAp618mF.js:1 diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 052d889..a398183 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -8,6 +8,7 @@ // {type:"makeTransfer"} mint a seat-transfer phrase // {type:"claimTransfer", code} claim a seat on a new device // {type:"catchUp", sinceSeq} replay of moves missed while away +// {type:"chat", text} table talk to the room // {type:"myGames", seats} summaries for held seats // {type:"stats"} the engagement tally // {type:"hotseatReport", ...} anonymous hotseat game counts @@ -17,6 +18,8 @@ // {type:"room", roomId, players, hostId, started, colors} // {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} @@ -33,6 +36,7 @@ import { getRoom, joinRoom, loadPersistedRooms, + addChat, makeTransferCode, redactFor, roomCount, @@ -246,6 +250,9 @@ 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; } @@ -271,6 +278,14 @@ wss.on("connection", (socket) => { broadcast(room, (playerId) => ({ type: "state", view: viewForPlayer(room, playerId), seq: room.log.length })); break; } + case "chat": { + const room = session.roomId ? getRoom(session.roomId) : undefined; + if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" }); + const result = addChat(room, session.playerId, String(msg.text ?? "")); + if ("error" in result) return send(socket, { type: "error", message: result.error }); + broadcast(room, () => ({ type: "chat", player: session.playerId, text: result.text, at: result.at })); + break; + } case "makeTransfer": { const room = session.roomId ? getRoom(session.roomId) : undefined; if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" }); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 464267b..ddd8da9 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -39,6 +39,8 @@ export interface Room { state: GameState | null; // null until started log: LoggedCommand[]; events: GameEvent[]; // full history (unredacted — redact per recipient) + /** Table talk, persisted with the room (public to all seats). */ + chat: { player: PlayerId; text: string; at: string }[]; } const rooms = new Map(); @@ -95,6 +97,7 @@ export function createRoom(hostId: PlayerId): { room: Room; token: string } { state: null, log: [], events: [], + chat: [], }; rooms.set(room.id, room); recordRoom(room); @@ -210,6 +213,20 @@ export function runCommand( return { events: result.events }; } +const CHAT_MAX_LENGTH = 300; +const CHAT_KEEP = 500; + +export function addChat(room: Room, playerId: PlayerId, rawText: string): { text: string; at: string } | { error: string } { + if (!room.players.includes(playerId)) return { error: "you hold no seat in this room" }; + const text = rawText.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, CHAT_MAX_LENGTH); + if (!text) return { error: "say something" }; + 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); + appendLine(room.id, { kind: "chat", player: playerId, text, at }); + return { text, at }; +} + export interface GameSummary { roomId: string; name: PlayerId; @@ -223,6 +240,8 @@ export interface GameSummary { attention: "turn" | "counteract" | "discard" | "interrupt" | null; round: number | null; lastMoveAt: string | null; + /** Total table-talk messages; the client tracks which it has seen. */ + chatCount: number; } /** A seat-holder's one-line view of a room, for the lobby ledger. */ @@ -252,6 +271,7 @@ export function summarize(room: Room, playerId: PlayerId): GameSummary { attention, round: s?.turn.round ?? null, lastMoveAt: room.log.length > 0 ? room.log[room.log.length - 1]!.at : null, + chatCount: room.chat.length, }; } @@ -264,6 +284,8 @@ export interface CatchUpStep { actor: PlayerId; events: GameEvent[]; view: GameView; + /** Table talk uttered between the previous command and this one. */ + chat?: { player: PlayerId; text: string }[]; } /** @@ -284,11 +306,16 @@ export function catchUpSteps(room: Room, playerId: PlayerId, sinceSeq: number, f if (!result.ok) return { error: `replay diverged at seq ${entry.seq}` }; current = result.state; if (entry.seq >= from) { + const prevAt = entry.seq > 0 ? room.log[entry.seq - 1]!.at : ""; + const said = room.chat + .filter((c) => c.at > prevAt && c.at <= entry.at) + .map((c) => ({ player: c.player, text: c.text })); steps.push({ seq: entry.seq, actor: entry.playerId, events: redactFor(result.events, playerId), view: viewFor(current, playerId), + ...(said.length > 0 ? { chat: said } : {}), }); } } @@ -396,6 +423,7 @@ export function loadPersistedRooms(): void { state: null, log: [], events: [], + chat: [], }; for (const line of lines.slice(1)) { if (line.kind === "join") { @@ -406,6 +434,8 @@ export function loadPersistedRooms(): void { } else if (line.kind === "start") { 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") { + room.chat.push({ player: line.player, text: line.text, at: line.at }); } 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/server/src/store.ts b/packages/server/src/store.ts index f046c6b..5d15c0c 100644 --- a/packages/server/src/store.ts +++ b/packages/server/src/store.ts @@ -43,7 +43,14 @@ export interface CommandLine { at: string; } -export type RoomLine = RoomMetaLine | JoinLine | StartLine | CommandLine; +export interface ChatLine { + kind: "chat"; + player: string; + text: string; + at: string; +} + +export type RoomLine = RoomMetaLine | JoinLine | StartLine | CommandLine | ChatLine; const DATA_DIR = process.env.WIZWAR_DATA_DIR ?? join(process.cwd(), "data", "rooms"); diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 0e04444..7db5102 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -28,6 +28,7 @@ let punchWallMode = $state(false); /** Leafing through the face-up discard pile. */ let showDiscards = $state(false); + let chatDraft = $state(""); let helpTab = $state<"play" | "rules" | "cards" | "about" | "tally">("play"); let hotseatCount = $state(2); let setupName = $state(""); @@ -961,9 +962,9 @@ {:else if !g.started} waiting to start · {g.players.join(", ")} {:else if g.yourTurn} - {attentionLabel(g.attention)} · round {g.round} · {timeAgo(g.lastMoveAt)} + {attentionLabel(g.attention)} · round {g.round} · {timeAgo(g.lastMoveAt)}{#if net.unreadChat(g.roomId, g.chatCount) > 0} · 💬{net.unreadChat(g.roomId, g.chatCount)}{/if} {:else} - {g.activePlayerId}'s turn · round {g.round} · {timeAgo(g.lastMoveAt)} + {g.activePlayerId}'s turn · round {g.round} · {timeAgo(g.lastMoveAt)}{#if net.unreadChat(g.roomId, g.chatCount) > 0} · 💬{net.unreadChat(g.roomId, g.chatCount)}{/if} {/if} @@ -1150,9 +1151,21 @@
{#each (local.active ? local.log : net.log).slice(-60) as line, i (i)} -
{line}
+
{line}
{/each}
+ {#if !local.active && net.roomId} +
{ + e.preventDefault(); + const t = chatDraft.trim(); + if (t) net.sendChat(t); + chatDraft = ""; + }}> + + +
+ {/if} @@ -1894,6 +1907,26 @@ .attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; } .attack-fist { font-size: 1.1rem; color: #43331f; } + .table-talk { + background: #efe8d4; + color: #43331f; + border-radius: 3px; + padding: 0.1rem 0.4rem; + margin: 0.15rem 0; + font-style: italic; + } + .say-box { display: flex; gap: 0.35rem; margin-top: 0.4rem; } + .say-input { + flex: 1; + min-width: 0; + background: #f6f0df; + border: 1px solid #b3a687; + border-radius: 4px; + padding: 0.35rem 0.55rem; + font-family: "Courier Prime", monospace; + font-size: 0.8rem; + color: #43331f; + } .discard-link { background: none; border: none; diff --git a/packages/web/src/Replay.svelte b/packages/web/src/Replay.svelte index 2114863..d142b30 100644 --- a/packages/web/src/Replay.svelte +++ b/packages/web/src/Replay.svelte @@ -7,7 +7,7 @@ steps, onclose, }: { - steps: { seq: number; actor: string; events: GameEvent[]; view: GameView }[]; + steps: { seq: number; actor: string; events: GameEvent[]; view: GameView; chat?: { player: string; text: string }[] }[]; onclose: () => void; } = $props(); @@ -52,7 +52,10 @@
{step.actor} {#each lines as line, i (i)}
{line}
{/each} - {#if lines.length === 0}
…considers the maze.
{/if} + {#if lines.length === 0 && !step.chat?.length}
…considers the maze.
{/if} + {#each step.chat ?? [] as c, i (i)} +
💬 {c.player}: {c.text}
+ {/each}
@@ -122,6 +125,7 @@ width: auto; max-width: 100%; } + .reel-talk { font-style: italic; opacity: 0.85; } .replay-caption { background: #efe8d4; color: #3a2f1f; diff --git a/packages/web/src/net.svelte.ts b/packages/web/src/net.svelte.ts index 79fc517..092272a 100644 --- a/packages/web/src/net.svelte.ts +++ b/packages/web/src/net.svelte.ts @@ -169,6 +169,12 @@ export function humanize(e: GameEvent): string | null { const SEAT_KEY = "wizwar-seat"; const SEATS_KEY = "wizwar-seats"; +const CHAT_SEEN_KEY = "wizwar-chat-seen"; + +function loadChatSeen(): Record { + try { return JSON.parse(localStorage.getItem(CHAT_SEEN_KEY) ?? "{}"); } + catch { return {}; } +} const SEEN_KEY = "wizwar-seen"; function loadSeen(): Record { @@ -189,6 +195,7 @@ export interface GameSummary { attention: "turn" | "counteract" | "discard" | "interrupt" | null; round: number | null; lastMoveAt: string | null; + chatCount: number; } export function attentionLabel(a: GameSummary["attention"]): string { @@ -224,6 +231,9 @@ class Net { seats = $state(loadSeats()); /** Lobby ledger: one summary per live seat. */ stats = $state | null>(null); + chatSeen = $state>(loadChatSeen()); + /** Messages in the current room this session (history + live). */ + chatCount = 0; games = $state([]); notificationsEnabled = $state( typeof Notification !== "undefined" && Notification.permission === "granted", @@ -233,7 +243,7 @@ class Net { /** Moves you haven't watched yet in the current room. */ missedMoves = $state(0); /** A catch-up reel delivered by the server. */ - catchUp = $state<{ seq: number; actor: string; events: GameEvent[]; view: GameView }[] | null>(null); + catchUp = $state<{ seq: number; actor: string; events: GameEvent[]; view: GameView; chat?: { player: string; text: string }[] }[] | null>(null); private seen: Record = loadSeen(); private currentSeq = 0; private lastYourTurn = new Map(); @@ -325,6 +335,23 @@ class Net { this.resume({ name: seat.name, roomId: seat.roomId, token: seat.token }); break; } + case "chat": { + this.log = [...this.log, `\u{1F4AC} ${msg.player}: ${msg.text}`]; + this.chatCount += 1; + 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; @@ -399,6 +426,24 @@ class Net { } /** Ask the server how all our games are doing. */ + sendChat(text: string): void { + this.send({ type: "chat", text }); + } + + /** Watching the table counts as reading the talk. */ + markChatSeen(): void { + if (!this.roomId) return; + const games = this.games.find((g) => g.roomId === this.roomId); + const count = Math.max(this.chatCount, games?.chatCount ?? 0); + if ((this.chatSeen[this.roomId] ?? 0) >= count) return; + this.chatSeen = { ...this.chatSeen, [this.roomId]: count }; + localStorage.setItem(CHAT_SEEN_KEY, JSON.stringify(this.chatSeen)); + } + + unreadChat(roomId: string, chatCount: number): number { + return Math.max(0, chatCount - (this.chatSeen[roomId] ?? 0)); + } + requestStats(): void { this.send({ type: "stats" }); }