Table talk: chat woven into the chronicle
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ed9585aa59
commit
df52e9b53a
@@ -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
|
[ 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
|
[ 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
|
[ 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
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
// {type:"makeTransfer"} mint a seat-transfer phrase
|
// {type:"makeTransfer"} mint a seat-transfer phrase
|
||||||
// {type:"claimTransfer", code} claim a seat on a new device
|
// {type:"claimTransfer", code} claim a seat on a new device
|
||||||
// {type:"catchUp", sinceSeq} replay of moves missed while away
|
// {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:"myGames", seats} summaries for held seats
|
||||||
// {type:"stats"} the engagement tally
|
// {type:"stats"} the engagement tally
|
||||||
// {type:"hotseatReport", ...} anonymous hotseat game counts
|
// {type:"hotseatReport", ...} anonymous hotseat game counts
|
||||||
@@ -17,6 +18,8 @@
|
|||||||
// {type:"room", roomId, players, hostId, started, colors}
|
// {type:"room", roomId, players, hostId, started, colors}
|
||||||
// {type:"events", events} redacted for this recipient
|
// {type:"events", events} redacted for this recipient
|
||||||
// {type:"state", view, seq} redacted full view (after every change)
|
// {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:"transferCode"|"transferClaimed"|"catchUp"|"games"|"stats"}
|
||||||
// {type:"error", message}
|
// {type:"error", message}
|
||||||
|
|
||||||
@@ -33,6 +36,7 @@ import {
|
|||||||
getRoom,
|
getRoom,
|
||||||
joinRoom,
|
joinRoom,
|
||||||
loadPersistedRooms,
|
loadPersistedRooms,
|
||||||
|
addChat,
|
||||||
makeTransferCode,
|
makeTransferCode,
|
||||||
redactFor,
|
redactFor,
|
||||||
roomCount,
|
roomCount,
|
||||||
@@ -246,6 +250,9 @@ wss.on("connection", (socket) => {
|
|||||||
if (room.state) {
|
if (room.state) {
|
||||||
send(socket, { type: "events", events: redactFor(room.events, name) });
|
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);
|
broadcastRoomState(room);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -271,6 +278,14 @@ wss.on("connection", (socket) => {
|
|||||||
broadcast(room, (playerId) => ({ type: "state", view: viewForPlayer(room, playerId), seq: room.log.length }));
|
broadcast(room, (playerId) => ({ type: "state", view: viewForPlayer(room, playerId), seq: room.log.length }));
|
||||||
break;
|
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": {
|
case "makeTransfer": {
|
||||||
const room = session.roomId ? getRoom(session.roomId) : undefined;
|
const room = session.roomId ? getRoom(session.roomId) : undefined;
|
||||||
if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" });
|
if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" });
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export interface Room {
|
|||||||
state: GameState | null; // null until started
|
state: GameState | null; // null until started
|
||||||
log: LoggedCommand[];
|
log: LoggedCommand[];
|
||||||
events: GameEvent[]; // full history (unredacted — redact per recipient)
|
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<string, Room>();
|
const rooms = new Map<string, Room>();
|
||||||
@@ -95,6 +97,7 @@ export function createRoom(hostId: PlayerId): { room: Room; token: string } {
|
|||||||
state: null,
|
state: null,
|
||||||
log: [],
|
log: [],
|
||||||
events: [],
|
events: [],
|
||||||
|
chat: [],
|
||||||
};
|
};
|
||||||
rooms.set(room.id, room);
|
rooms.set(room.id, room);
|
||||||
recordRoom(room);
|
recordRoom(room);
|
||||||
@@ -210,6 +213,20 @@ export function runCommand(
|
|||||||
return { events: result.events };
|
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 {
|
export interface GameSummary {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
name: PlayerId;
|
name: PlayerId;
|
||||||
@@ -223,6 +240,8 @@ export interface GameSummary {
|
|||||||
attention: "turn" | "counteract" | "discard" | "interrupt" | null;
|
attention: "turn" | "counteract" | "discard" | "interrupt" | null;
|
||||||
round: number | null;
|
round: number | null;
|
||||||
lastMoveAt: string | 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. */
|
/** 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,
|
attention,
|
||||||
round: s?.turn.round ?? null,
|
round: s?.turn.round ?? null,
|
||||||
lastMoveAt: room.log.length > 0 ? room.log[room.log.length - 1]!.at : 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;
|
actor: PlayerId;
|
||||||
events: GameEvent[];
|
events: GameEvent[];
|
||||||
view: GameView;
|
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}` };
|
if (!result.ok) return { error: `replay diverged at seq ${entry.seq}` };
|
||||||
current = result.state;
|
current = result.state;
|
||||||
if (entry.seq >= from) {
|
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({
|
steps.push({
|
||||||
seq: entry.seq,
|
seq: entry.seq,
|
||||||
actor: entry.playerId,
|
actor: entry.playerId,
|
||||||
events: redactFor(result.events, playerId),
|
events: redactFor(result.events, playerId),
|
||||||
view: viewFor(current, playerId),
|
view: viewFor(current, playerId),
|
||||||
|
...(said.length > 0 ? { chat: said } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,6 +423,7 @@ export function loadPersistedRooms(): void {
|
|||||||
state: null,
|
state: null,
|
||||||
log: [],
|
log: [],
|
||||||
events: [],
|
events: [],
|
||||||
|
chat: [],
|
||||||
};
|
};
|
||||||
for (const line of lines.slice(1)) {
|
for (const line of lines.slice(1)) {
|
||||||
if (line.kind === "join") {
|
if (line.kind === "join") {
|
||||||
@@ -406,6 +434,8 @@ export function loadPersistedRooms(): void {
|
|||||||
} else if (line.kind === "start") {
|
} else if (line.kind === "start") {
|
||||||
const r = startInMemory(room, line.expansion, line.colors, line.deckRev);
|
const r = startInMemory(room, line.expansion, line.colors, line.deckRev);
|
||||||
if ("error" in r) throw new Error(`replay start failed: ${r.error}`);
|
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") {
|
} else if (line.kind === "command") {
|
||||||
if (!room.state) throw new Error("command before start in log");
|
if (!room.state) throw new Error("command before start in log");
|
||||||
const result = applyCommand(room.state, line.playerId, line.command as Command);
|
const result = applyCommand(room.state, line.playerId, line.command as Command);
|
||||||
|
|||||||
@@ -43,7 +43,14 @@ export interface CommandLine {
|
|||||||
at: string;
|
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");
|
const DATA_DIR = process.env.WIZWAR_DATA_DIR ?? join(process.cwd(), "data", "rooms");
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
let punchWallMode = $state(false);
|
let punchWallMode = $state(false);
|
||||||
/** Leafing through the face-up discard pile. */
|
/** Leafing through the face-up discard pile. */
|
||||||
let showDiscards = $state(false);
|
let showDiscards = $state(false);
|
||||||
|
let chatDraft = $state("");
|
||||||
let helpTab = $state<"play" | "rules" | "cards" | "about" | "tally">("play");
|
let helpTab = $state<"play" | "rules" | "cards" | "about" | "tally">("play");
|
||||||
let hotseatCount = $state(2);
|
let hotseatCount = $state(2);
|
||||||
let setupName = $state("");
|
let setupName = $state("");
|
||||||
@@ -961,9 +962,9 @@
|
|||||||
{:else if !g.started}
|
{:else if !g.started}
|
||||||
waiting to start · {g.players.join(", ")}
|
waiting to start · {g.players.join(", ")}
|
||||||
{:else if g.yourTurn}
|
{: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}
|
{: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}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -1150,9 +1151,21 @@
|
|||||||
|
|
||||||
<div class="chronicle" aria-label="game log" bind:this={chronicleEl}>
|
<div class="chronicle" aria-label="game log" bind:this={chronicleEl}>
|
||||||
{#each (local.active ? local.log : net.log).slice(-60) as line, i (i)}
|
{#each (local.active ? local.log : net.log).slice(-60) as line, i (i)}
|
||||||
<div>{line}</div>
|
<div class:table-talk={line.startsWith("\u{1F4AC}")}>{line}</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{#if !local.active && net.roomId}
|
||||||
|
<form class="say-box" onsubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const t = chatDraft.trim();
|
||||||
|
if (t) net.sendChat(t);
|
||||||
|
chatDraft = "";
|
||||||
|
}}>
|
||||||
|
<input class="say-input" bind:value={chatDraft} maxlength="300"
|
||||||
|
placeholder="say something to the table…" aria-label="table talk" />
|
||||||
|
<button class="stamp tiny" type="submit" disabled={!chatDraft.trim()}>say</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1894,6 +1907,26 @@
|
|||||||
.attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; }
|
.attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; }
|
||||||
.attack-fist { font-size: 1.1rem; color: #43331f; }
|
.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 {
|
.discard-link {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
steps,
|
steps,
|
||||||
onclose,
|
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;
|
onclose: () => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -52,7 +52,10 @@
|
|||||||
<div class="replay-caption">
|
<div class="replay-caption">
|
||||||
<strong>{step.actor}</strong>
|
<strong>{step.actor}</strong>
|
||||||
{#each lines as line, i (i)}<div>{line}</div>{/each}
|
{#each lines as line, i (i)}<div>{line}</div>{/each}
|
||||||
{#if lines.length === 0}<div>…considers the maze.</div>{/if}
|
{#if lines.length === 0 && !step.chat?.length}<div>…considers the maze.</div>{/if}
|
||||||
|
{#each step.chat ?? [] as c, i (i)}
|
||||||
|
<div class="reel-talk">💬 {c.player}: {c.text}</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="replay-controls">
|
<div class="replay-controls">
|
||||||
<button onclick={() => { playing = false; idx = Math.max(0, idx - 1); }} aria-label="previous move">◀</button>
|
<button onclick={() => { playing = false; idx = Math.max(0, idx - 1); }} aria-label="previous move">◀</button>
|
||||||
@@ -122,6 +125,7 @@
|
|||||||
width: auto;
|
width: auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
.reel-talk { font-style: italic; opacity: 0.85; }
|
||||||
.replay-caption {
|
.replay-caption {
|
||||||
background: #efe8d4;
|
background: #efe8d4;
|
||||||
color: #3a2f1f;
|
color: #3a2f1f;
|
||||||
|
|||||||
@@ -169,6 +169,12 @@ export function humanize(e: GameEvent): string | null {
|
|||||||
|
|
||||||
const SEAT_KEY = "wizwar-seat";
|
const SEAT_KEY = "wizwar-seat";
|
||||||
const SEATS_KEY = "wizwar-seats";
|
const SEATS_KEY = "wizwar-seats";
|
||||||
|
const CHAT_SEEN_KEY = "wizwar-chat-seen";
|
||||||
|
|
||||||
|
function loadChatSeen(): Record<string, number> {
|
||||||
|
try { return JSON.parse(localStorage.getItem(CHAT_SEEN_KEY) ?? "{}"); }
|
||||||
|
catch { return {}; }
|
||||||
|
}
|
||||||
const SEEN_KEY = "wizwar-seen";
|
const SEEN_KEY = "wizwar-seen";
|
||||||
|
|
||||||
function loadSeen(): Record<string, number> {
|
function loadSeen(): Record<string, number> {
|
||||||
@@ -189,6 +195,7 @@ export interface GameSummary {
|
|||||||
attention: "turn" | "counteract" | "discard" | "interrupt" | null;
|
attention: "turn" | "counteract" | "discard" | "interrupt" | null;
|
||||||
round: number | null;
|
round: number | null;
|
||||||
lastMoveAt: string | null;
|
lastMoveAt: string | null;
|
||||||
|
chatCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function attentionLabel(a: GameSummary["attention"]): string {
|
export function attentionLabel(a: GameSummary["attention"]): string {
|
||||||
@@ -224,6 +231,9 @@ class Net {
|
|||||||
seats = $state<Seat[]>(loadSeats());
|
seats = $state<Seat[]>(loadSeats());
|
||||||
/** Lobby ledger: one summary per live seat. */
|
/** Lobby ledger: one summary per live seat. */
|
||||||
stats = $state<Record<string, number | string | null> | null>(null);
|
stats = $state<Record<string, number | string | null> | null>(null);
|
||||||
|
chatSeen = $state<Record<string, number>>(loadChatSeen());
|
||||||
|
/** Messages in the current room this session (history + live). */
|
||||||
|
chatCount = 0;
|
||||||
games = $state<GameSummary[]>([]);
|
games = $state<GameSummary[]>([]);
|
||||||
notificationsEnabled = $state(
|
notificationsEnabled = $state(
|
||||||
typeof Notification !== "undefined" && Notification.permission === "granted",
|
typeof Notification !== "undefined" && Notification.permission === "granted",
|
||||||
@@ -233,7 +243,7 @@ class Net {
|
|||||||
/** Moves you haven't watched yet in the current room. */
|
/** Moves you haven't watched yet in the current room. */
|
||||||
missedMoves = $state(0);
|
missedMoves = $state(0);
|
||||||
/** A catch-up reel delivered by the server. */
|
/** 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<string, number> = loadSeen();
|
private seen: Record<string, number> = loadSeen();
|
||||||
private currentSeq = 0;
|
private currentSeq = 0;
|
||||||
private lastYourTurn = new Map<string, boolean>();
|
private lastYourTurn = new Map<string, boolean>();
|
||||||
@@ -325,6 +335,23 @@ class Net {
|
|||||||
this.resume({ name: seat.name, roomId: seat.roomId, token: seat.token });
|
this.resume({ name: seat.name, roomId: seat.roomId, token: seat.token });
|
||||||
break;
|
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": {
|
case "stats": {
|
||||||
this.stats = msg.stats;
|
this.stats = msg.stats;
|
||||||
break;
|
break;
|
||||||
@@ -399,6 +426,24 @@ class Net {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Ask the server how all our games are doing. */
|
/** 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 {
|
requestStats(): void {
|
||||||
this.send({ type: "stats" });
|
this.send({ type: "stats" });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user