The opening roll-off steps into the light
First wizard was never random: createGame has always rolled a die per player from the seed, re-rolling ties, and the gameStarted event carried every pip — straight into a log line that said only "X goes first." Now the table sees the ceremony: a dice modal as the boards flip, one row per wizard, the trophy on the high roll, a note when tied wizards rolled again — in online games and hotseat alike. The chronicle line keeps the full roll-off too. Pure presentation; the dice were always honest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2950380e23
commit
22153b1e1b
@@ -27,6 +27,11 @@
|
|||||||
}
|
}
|
||||||
/** Which pending attack the player has already acknowledged (modal dismissed). */
|
/** Which pending attack the player has already acknowledged (modal dismissed). */
|
||||||
let attackNoticeSeen = $state<string | null>(null);
|
let attackNoticeSeen = $state<string | null>(null);
|
||||||
|
const openingRolls = $derived(net.openingRolls ?? local.openingRolls);
|
||||||
|
function dismissRolls() {
|
||||||
|
net.openingRolls = null;
|
||||||
|
local.openingRolls = null;
|
||||||
|
}
|
||||||
/** A face-up card being peeked at from the scoresheet (view-only). */
|
/** A face-up card being peeked at from the scoresheet (view-only). */
|
||||||
let peekCard = $state<CardInstance | null>(null);
|
let peekCard = $state<CardInstance | null>(null);
|
||||||
/** When the peeked card is a creature on the board, its live stats ride along. */
|
/** When the peeked card is a creature on the board, its live stats ride along. */
|
||||||
@@ -887,6 +892,28 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if openingRolls}
|
||||||
|
<div class="scrim attack-scrim" role="alertdialog" aria-label="the roll for first wizard">
|
||||||
|
<div class="attack-notice">
|
||||||
|
<div class="attack-headline">The dice decide who casts first</div>
|
||||||
|
<div class="rolloff">
|
||||||
|
{#each openingRolls.players as p (p)}
|
||||||
|
<div class="rolloff-row" class:rolloff-winner={p === openingRolls.first}>
|
||||||
|
<span class="rolloff-name">{p === openingRolls.first ? "🏆 " : ""}{p}</span>
|
||||||
|
<span class="rolloff-dice">
|
||||||
|
{#each openingRolls.rolls[p] ?? [] as r, i (i)}<span class="die-face">🎲{r}</span>{/each}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if Object.values(openingRolls.rolls).some((r) => r.length > 1)}
|
||||||
|
<div class="rolloff-note">Tied wizards rolled again.</div>
|
||||||
|
{/if}
|
||||||
|
<button class="stamp big" onclick={dismissRolls}>{openingRolls.first} leads — begin!</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if view?.stack && attackNoticeKey && attackNoticeSeen !== attackNoticeKey && !noFanfare}
|
{#if view?.stack && attackNoticeKey && attackNoticeSeen !== attackNoticeKey && !noFanfare}
|
||||||
<div class="scrim attack-scrim" role="alertdialog" aria-label="you are under attack">
|
<div class="scrim attack-scrim" role="alertdialog" aria-label="you are under attack">
|
||||||
<div class="attack-notice">
|
<div class="attack-notice">
|
||||||
@@ -2094,6 +2121,24 @@
|
|||||||
margin: 0.3rem auto 0;
|
margin: 0.3rem auto 0;
|
||||||
}
|
}
|
||||||
.attack-standing.nullified { color: #b3372b; }
|
.attack-standing.nullified { color: #b3372b; }
|
||||||
|
.rolloff {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin: 0.4rem 0 0.6rem;
|
||||||
|
}
|
||||||
|
.rolloff-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1.2rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #43331f;
|
||||||
|
}
|
||||||
|
.rolloff-winner { font-weight: 700; }
|
||||||
|
.rolloff-name { font-family: "Oswald", sans-serif; letter-spacing: 0.04em; }
|
||||||
|
.rolloff-dice { font-family: "Courier Prime", monospace; }
|
||||||
|
.die-face { margin-left: 0.4rem; }
|
||||||
|
.rolloff-note { font-size: 0.8rem; color: #6b5a41; font-style: italic; margin-bottom: 0.3rem; }
|
||||||
.attack-fist { font-size: 1.1rem; color: #43331f; }
|
.attack-fist { font-size: 1.1rem; color: #43331f; }
|
||||||
|
|
||||||
.table-talk {
|
.table-talk {
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ class LocalGame {
|
|||||||
handoffTo = $state<PlayerId | null>(null);
|
handoffTo = $state<PlayerId | null>(null);
|
||||||
log = $state<string[]>([]);
|
log = $state<string[]>([]);
|
||||||
/** The finished game as a reel, each step from its actor's own seat. */
|
/** The finished game as a reel, each step from its actor's own seat. */
|
||||||
|
/** The opening roll-off, shown once as the boards flip. */
|
||||||
|
openingRolls = $state<{ rolls: Record<string, number[]>; first: string; players: string[] } | null>(null);
|
||||||
replaySteps = $state<{ seq: number; actor: PlayerId; events: GameEvent[]; view: GameView }[] | null>(null);
|
replaySteps = $state<{ seq: number; actor: PlayerId; events: GameEvent[]; view: GameView }[] | null>(null);
|
||||||
view = $derived(
|
view = $derived(
|
||||||
this.gameState && this.viewerId ? viewFor(this.gameState, this.viewerId) : null,
|
this.gameState && this.viewerId ? viewFor(this.gameState, this.viewerId) : null,
|
||||||
@@ -135,6 +137,11 @@ class LocalGame {
|
|||||||
deckRev: 13,
|
deckRev: 13,
|
||||||
};
|
};
|
||||||
const { state, events } = createGame(config);
|
const { state, events } = createGame(config);
|
||||||
|
for (const e of events) {
|
||||||
|
if (e.type === "gameStarted") {
|
||||||
|
this.openingRolls = { rolls: e.dieRolls, first: e.firstPlayer, players: e.players };
|
||||||
|
}
|
||||||
|
}
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.commands = [];
|
this.commands = [];
|
||||||
this.gameState = state;
|
this.gameState = state;
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ export function spellName(cardId: string): string {
|
|||||||
|
|
||||||
export function humanize(e: GameEvent): string | null {
|
export function humanize(e: GameEvent): string | null {
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case "gameStarted": return `Game started — ${e.players.join(", ")}. ${e.firstPlayer} goes first.`;
|
case "gameStarted": {
|
||||||
|
const rolloff = e.players
|
||||||
|
.map((p) => `${p} 🎲 ${(e.dieRolls[p] ?? []).join(", ")}`)
|
||||||
|
.join(" · ");
|
||||||
|
return `Game started. The roll-off: ${rolloff} — ${e.firstPlayer} goes first.`;
|
||||||
|
}
|
||||||
case "turnStarted": return `— ${e.player}'s turn (round ${e.round}) —`;
|
case "turnStarted": return `— ${e.player}'s turn (round ${e.round}) —`;
|
||||||
case "turnSkipped": return `${e.player} loses a turn.`;
|
case "turnSkipped": return `${e.player} loses a turn.`;
|
||||||
case "extraTurnStarted": return `${e.player} takes an extra turn!`;
|
case "extraTurnStarted": return `${e.player} takes an extra turn!`;
|
||||||
@@ -246,6 +251,8 @@ 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. */
|
||||||
|
/** The opening roll-off, shown once as the boards flip. */
|
||||||
|
openingRolls = $state<{ rolls: Record<string, number[]>; first: string; players: string[] } | null>(null);
|
||||||
catchUp = $state<{ seq: number; actor: string; events: GameEvent[]; view: GameView; chat?: { player: string; text: string }[] }[] | 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;
|
||||||
@@ -327,6 +334,9 @@ class Net {
|
|||||||
let talk = 0;
|
let talk = 0;
|
||||||
for (const e of msg.events as GameEvent[]) {
|
for (const e of msg.events as GameEvent[]) {
|
||||||
if (e.type === "tableTalk") talk++;
|
if (e.type === "tableTalk") talk++;
|
||||||
|
if (e.type === "gameStarted") {
|
||||||
|
this.openingRolls = { rolls: e.dieRolls, first: e.firstPlayer, players: e.players };
|
||||||
|
}
|
||||||
const line = humanize(e);
|
const line = humanize(e);
|
||||||
if (line) this.log = [...this.log, line];
|
if (line) this.log = [...this.log, line];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user