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:
Eric Wagoner
2026-08-16 14:24:22 -04:00
co-authored by Claude Fable 5
parent ed9585aa59
commit df52e9b53a
7 changed files with 242 additions and 7 deletions
+6 -2
View File
@@ -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 @@
<div class="replay-caption">
<strong>{step.actor}</strong>
{#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 class="replay-controls">
<button onclick={() => { playing = false; idx = Math.max(0, idx - 1); }} aria-label="previous move"></button>
@@ -122,6 +125,7 @@
width: auto;
max-width: 100%;
}
.reel-talk { font-style: italic; opacity: 0.85; }
.replay-caption {
background: #efe8d4;
color: #3a2f1f;