From 8582b7feb7b760eb3ce3ccf401499fa0f28128a8 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Thu, 17 Sep 2026 00:22:54 -0400 Subject: [PATCH] What you missed, in facts; a timeline on the reel; room for table talk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A returning player's slip now lists what happened while they were away — "Automaton took your treasure.", "You lost 3 life.", "It's your turn." — each fact opening the catch-up reel at its moment, read from the missed steps as the server redacted them for that seat; when nothing touched them it says what each wizard was up to instead. The reel gains a timeline: a tick per move in the mover's color, taller where a turn begins, a blow lands, or gold changes hands, and any tick jumps the reel there. The chronicle takes a filter — All, Game, Table talk — with an unread count on the talk that arrived while the casting panel covered it (a wizard's own words never count), a peek at the latest line above the composer while the panel is up, and the composer itself no longer leaves when a response is owed. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG --- packages/web/src/App.svelte | 84 ++++++++++++++++++++++--- packages/web/src/Replay.svelte | 51 ++++++++++++++- packages/web/src/catchup.ts | 109 +++++++++++++++++++++++++++++++++ packages/web/src/net.svelte.ts | 31 ++++++++-- 4 files changed, 262 insertions(+), 13 deletions(-) create mode 100644 packages/web/src/catchup.ts diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 696c52d..fbe55b4 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -10,6 +10,7 @@ import Card from "./Card.svelte"; import Help from "./Help.svelte"; import type { HelpTab } from "./help-state"; + import { summarizeMissed } from "./catchup"; import Replay from "./Replay.svelte"; import FxGallery from "./FxGallery.svelte"; import TokenGallery from "./TokenGallery.svelte"; @@ -1476,6 +1477,26 @@ return view ? view.treasures.filter((t) => t.owner !== p.id && t.position && t.position.x === p.home.x && t.position.y === p.home.y).length : 0; } + /** What the chronicle shows: everything, the game's own lines, or the talk. */ + let logFilter = $state<"all" | "game" | "talk">("all"); + const isTalk = (text: string) => text.startsWith("\u{1F4AC}") || text.startsWith("\u{1F3B2}"); + function showLine(line: { text: string }): boolean { + return logFilter === "all" ? true : logFilter === "talk" ? isTalk(line.text) : !isTalk(line.text); + } + /** Talk that arrived while the chronicle was covered or filtered away. */ + let seenTalk = $state(0); + $effect(() => { + const count = net.chatCount; + if ((!castPanelOn && logFilter !== "game") || net.lastTalkBy === net.you) seenTalk = count; + }); + const unreadTalk = $derived(Math.max(0, net.chatCount - seenTalk)); + const lastTalk = $derived.by(() => { + for (let i = net.log.length - 1; i >= 0; i--) { + const t = net.log[i]!.text; + if (t.startsWith("\u{1F4AC}")) return t.slice(2).trim(); + } + return null; + }); /** The table is out of reach: play waits, and nothing here can be sent. */ const offline = $derived(!local.active && net.roomId != null && net.status !== "connected"); // Talk that never arrived goes back into the composer, with a word. @@ -2354,7 +2375,7 @@ net.requestShare()} onclose={() => net.closeMoment()} /> {:else if net.catchUp && net.catchUp.length > 0} - net.requestShare(-1) : null} onclose={() => net.closeCatchUp()} /> {:else if local.replaySteps && local.replaySteps.length > 0} @@ -2983,9 +3004,24 @@