What you missed, in facts; a timeline on the reel; room for table talk
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
80dd7a7865
commit
8582b7feb7
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { motion } from "./motion";
|
||||
import Board from "./Board.svelte";
|
||||
import { wizardColor } from "./colors";
|
||||
import FirstPerson from "./fpv/FirstPerson.svelte";
|
||||
import { untrack } from "svelte";
|
||||
import { humanize, spellName } from "./net.svelte";
|
||||
@@ -19,6 +20,7 @@
|
||||
pov = null,
|
||||
onshare = null,
|
||||
endLabel = null,
|
||||
startAt = 0,
|
||||
}: {
|
||||
steps: { seq: number; actor: string; events: GameEvent[]; view: GameView; chat?: { player: string; text: string }[] }[];
|
||||
onclose: () => void;
|
||||
@@ -32,6 +34,8 @@
|
||||
onshare?: (() => Promise<string>) | null;
|
||||
/** What the leave button says once the reel has run out. */
|
||||
endLabel?: string | null;
|
||||
/** The step the reel opens on. */
|
||||
startAt?: number;
|
||||
} = $props();
|
||||
|
||||
/** The share button's little life: offer, mint, report. */
|
||||
@@ -49,7 +53,20 @@
|
||||
setTimeout(() => (shareState = "idle"), 4000);
|
||||
}
|
||||
|
||||
let idx = $state(0);
|
||||
let idx = $state(untrack(() => Math.min(Math.max(0, startAt), Math.max(0, steps.length - 1))));
|
||||
/** The timeline: one tick a step, a turn's first step marked, and the
|
||||
* steps where blows landed or gold changed hands flagged, so a reel of
|
||||
* a hundred moves can be read at a glance and jumped into. */
|
||||
const ticks = $derived(steps.map((s, i) => {
|
||||
let flag: "turn" | "hit" | "gold" | "end" | null = null;
|
||||
for (const e of s.events) {
|
||||
if (e.type === "gameWon" || e.type === "playerEliminated") { flag = "end"; break; }
|
||||
if (e.type === "treasurePickedUp" || (e.type === "treasureDropped" && e.onHomeOf != null)) flag = "gold";
|
||||
else if (!flag && (e.type === "damaged" || e.type === "attackResolved" || e.type === "punched")) flag = "hit";
|
||||
else if (!flag && e.type === "turnStarted") flag = "turn";
|
||||
}
|
||||
return { i, actor: s.actor, flag, color: wizardColor(s.view, s.actor) };
|
||||
}));
|
||||
let playing = $state(true);
|
||||
let speed = $state(1);
|
||||
/** Watch the board from above, or relive it through your own eyes. */
|
||||
@@ -748,6 +765,15 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if steps.length > 3}
|
||||
<div class="timeline" role="group" aria-label="the moves, a tick each">
|
||||
{#each ticks as t (t.i)}
|
||||
<button class="tick" class:current={t.i === idx} class:turn={t.flag === "turn"} class:hit={t.flag === "hit"} class:gold={t.flag === "gold"} class:end={t.flag === "end"}
|
||||
style:--who={t.color} title={`move ${t.i + 1} — ${t.actor}${t.flag ? ` · ${t.flag === "turn" ? "turn begins" : t.flag === "hit" ? "a blow" : t.flag === "gold" ? "treasure" : "the end"}` : ""}`}
|
||||
onclick={() => { playing = false; idx = t.i; }} aria-label={`jump to move ${t.i + 1}`}></button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="replay-controls">
|
||||
<button onclick={() => { playing = false; idx = Math.max(0, idx - 1); }} aria-label="previous move">◀</button>
|
||||
<button class="playpause" onclick={() => (playing = !playing)} aria-label={playing ? "pause" : "play"}>
|
||||
@@ -931,6 +957,29 @@
|
||||
opacity: 0.7;
|
||||
}
|
||||
.replay-controls .speed.current { opacity: 1; text-decoration: underline; }
|
||||
.timeline {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 1px;
|
||||
height: 14px;
|
||||
margin: 0.4rem 0.2rem 0.2rem;
|
||||
}
|
||||
.tick {
|
||||
flex: 1 1 0;
|
||||
min-width: 2px;
|
||||
height: 6px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 1px;
|
||||
background: var(--who, #8d8672);
|
||||
opacity: 0.45;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tick.turn { height: 9px; opacity: 0.7; }
|
||||
.tick.hit { height: 12px; opacity: 0.9; }
|
||||
.tick.gold { height: 14px; opacity: 1; box-shadow: 0 0 0 1px #e0b34a; }
|
||||
.tick.end { height: 14px; opacity: 1; background: #e9e1cb; }
|
||||
.tick.current { opacity: 1; outline: 1px solid #fff; outline-offset: 1px; }
|
||||
.replay-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
Reference in New Issue
Block a user