The die rolls in the open

Every player-facing D4 roll now lands in the chronicle as pips with
its purpose: "🎲 alice rolls a 3 — aiming at the unseen — only a 1
finds them." A shared rollD4 helper threads the generic dieRolled
event through blind staggers and swings, pit leaps, ooze and pit
struggles, and the invisible/shrink miss rolls — the table sees the
die land, exactly as it would in person. Rolls whose numbers already
showed (the troll's swing, the misdirection direction, the opening
roll-off) keep their lines.

And the physical die's other job — "any 50-50 call" — gets its
button: 🎲 beside the say-box in online games (server-rolled,
published through the chat ledger so it persists, replays, and counts
toward unread badges) and under the chronicle in hotseat, rolled on
the device. Chronicle only; no game state touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 15:57:28 -04:00
co-authored by Claude Fable 5
parent d6d34887e3
commit ef661c79ad
6 changed files with 221 additions and 17 deletions
+8
View File
@@ -1189,6 +1189,12 @@
<div class:table-talk={line.startsWith("\u{1F4AC}")}>{line}</div>
{/each}
</div>
{#if local.active && local.viewerId}
<div class="say-box">
<button class="stamp tiny" type="button" title="roll the die (house calls)"
onclick={() => local.rollTableDie()}>🎲 roll the die</button>
</div>
{/if}
{#if !local.active && net.roomId}
<form class="say-box" onsubmit={(e) => {
e.preventDefault();
@@ -1199,6 +1205,8 @@
<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>
<button class="stamp tiny" type="button" title="roll the die (house calls)"
onclick={() => net.rollTableDie()}>🎲</button>
</form>
{/if}
</aside>
+7
View File
@@ -60,6 +60,13 @@ class LocalGame {
private activeMs = 0;
private lastMoveAt = 0;
/** The tabletop D4 for house calls: chronicle only, no game state. */
rollTableDie(): void {
if (!this.viewerId) return;
const roll = 1 + (crypto.getRandomValues(new Uint32Array(1))[0]! % 4);
this.log = [...this.log, `\u{1F3B2} ${this.viewerId} rolls the die \u2014 ${roll}`];
}
/** Rebuild the whole game as replay steps (finished games only). */
buildReplay(): void {
if (!this.config || this.gameState?.phase !== "finished") return;
+5
View File
@@ -145,6 +145,7 @@ export function humanize(e: GameEvent): string | null {
case "objectPickedUp": return `${e.player} picks up the ${cardDef(e.card.cardId).name} — actions over.`;
case "wardSet": return e.armed ? "Your ward is set — the next thief bleeds." : "Your ward stands down.";
case "chaosShielded": return `${e.player} raises a FULL SHIELD and sits out the chaos.`;
case "dieRolled": return `\u{1F3B2} ${e.player ?? "The maze"} rolls a ${e.roll}${e.purpose}.`;
case "pushed": return e.player
? `${e.by} shoves ${e.player} down the corridor!`
: `${e.by} shoves the beast down the corridor!`;
@@ -428,6 +429,10 @@ class Net {
}
/** Ask the server how all our games are doing. */
rollTableDie(): void {
this.send({ type: "rollDie" });
}
sendChat(text: string): void {
this.send({ type: "chat", text });
}