The finished table lays every hand face-up
Once the game is won, the view stops keeping secrets: revealedHands carries every player's cards, and the table replaces your hand row with a face-up gallery — each wizard's name (trophy on the winner) beside their final hand, tap any card to enlarge, "empty-handed" written in script for those who died with nothing. In hotseat this is the thing the winner passes around after dismissing the fanfare; in async games it greets each player when they open the finished game. Redaction still guards every phase before "finished." Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
13aeeea0ef
commit
e7b17f7643
@@ -67,6 +67,8 @@ export interface GameView {
|
|||||||
outOfTurnWindow: { playerId: PlayerId; kind: "interrupt" | "opportunity-fire" } | null;
|
outOfTurnWindow: { playerId: PlayerId; kind: "interrupt" | "opportunity-fire" } | null;
|
||||||
/** YOUR armed ambushes. Other players' ambushes are invisible. */
|
/** YOUR armed ambushes. Other players' ambushes are invisible. */
|
||||||
yourAmbushes: AmbushState[];
|
yourAmbushes: AmbushState[];
|
||||||
|
/** Once the game is finished, every hand goes face-up on the table. */
|
||||||
|
revealedHands: Record<PlayerId, CardInstance[]> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function viewFor(state: GameState, playerId: PlayerId): GameView {
|
export function viewFor(state: GameState, playerId: PlayerId): GameView {
|
||||||
@@ -107,6 +109,9 @@ export function viewFor(state: GameState, playerId: PlayerId): GameView {
|
|||||||
displayed: p.hand.filter((c) => p.displayed.includes(c.instanceId)),
|
displayed: p.hand.filter((c) => p.displayed.includes(c.instanceId)),
|
||||||
})),
|
})),
|
||||||
yourHand: you ? [...you.hand] : [],
|
yourHand: you ? [...you.hand] : [],
|
||||||
|
revealedHands: state.phase === "finished"
|
||||||
|
? Object.fromEntries(state.players.map((p) => [p.id, [...p.hand]]))
|
||||||
|
: null,
|
||||||
treasures: state.treasures.map((t) => ({ ...t })),
|
treasures: state.treasures.map((t) => ({ ...t })),
|
||||||
deckCount: state.deck.length,
|
deckCount: state.deck.length,
|
||||||
discardCount: state.discard.length,
|
discardCount: state.discard.length,
|
||||||
|
|||||||
@@ -1212,8 +1212,29 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if view.phase === "finished" && view.revealedHands}
|
||||||
|
<div class="final-reveal" aria-label="all hands revealed">
|
||||||
|
<div class="reveal-head">The hands, face-up</div>
|
||||||
|
{#each view.players as p (p.id)}
|
||||||
|
<div class="reveal-row">
|
||||||
|
<span class="reveal-name" class:reveal-winner={p.id === view.winner}>
|
||||||
|
{p.id === view.winner ? "🏆 " : ""}{p.id}
|
||||||
|
</span>
|
||||||
|
<div class="reveal-cards">
|
||||||
|
{#each view.revealedHands[p.id] ?? [] as card (card.instanceId)}
|
||||||
|
<Card {card} onclick={() => (peekCard = card)} />
|
||||||
|
{/each}
|
||||||
|
{#if (view.revealedHands[p.id] ?? []).length === 0}
|
||||||
|
<span class="reveal-empty">empty-handed</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="hand" aria-label="your hand">
|
<div class="hand" aria-label="your hand">
|
||||||
{#each view.yourHand as card (card.instanceId)}
|
{#each view.phase === "finished" ? [] : view.yourHand as card (card.instanceId)}
|
||||||
<Card
|
<Card
|
||||||
{card}
|
{card}
|
||||||
selected={selectedCard?.instanceId === card.instanceId ||
|
selected={selectedCard?.instanceId === card.instanceId ||
|
||||||
@@ -1771,6 +1792,36 @@
|
|||||||
max-width: 11rem;
|
max-width: 11rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.final-reveal {
|
||||||
|
background: #efe8d4;
|
||||||
|
border: 1px solid #b3a687;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.7rem 0.9rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
.reveal-head {
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #43331f;
|
||||||
|
border-bottom: 1px solid #b3a687;
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.reveal-row { display: flex; align-items: flex-start; gap: 0.6rem; margin-bottom: 0.5rem; }
|
||||||
|
.reveal-name {
|
||||||
|
font-family: "Courier Prime", monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #43331f;
|
||||||
|
min-width: 6.5rem;
|
||||||
|
padding-top: 0.4rem;
|
||||||
|
}
|
||||||
|
.reveal-name.reveal-winner { font-weight: 700; }
|
||||||
|
.reveal-cards { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
||||||
|
.reveal-cards :global(.card) { transform: scale(0.82); transform-origin: top left; margin: -0.35rem -0.8rem -1.1rem 0; }
|
||||||
|
.reveal-empty { font-family: "Caveat", cursive; color: #8a7a5e; padding-top: 0.5rem; }
|
||||||
|
|
||||||
.victory-notice { border-color: #a5842c; box-shadow: 0 0 0 4px rgba(201, 167, 42, 0.35), 0 18px 50px rgba(0, 0, 0, 0.6); }
|
.victory-notice { border-color: #a5842c; box-shadow: 0 0 0 4px rgba(201, 167, 42, 0.35), 0 18px 50px rgba(0, 0, 0, 0.6); }
|
||||||
.victory-trophy { font-size: 3rem; line-height: 1; }
|
.victory-trophy { font-size: 3rem; line-height: 1; }
|
||||||
.victory-name {
|
.victory-name {
|
||||||
@@ -1861,6 +1912,36 @@
|
|||||||
max-width: 11rem;
|
max-width: 11rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.final-reveal {
|
||||||
|
background: #efe8d4;
|
||||||
|
border: 1px solid #b3a687;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.7rem 0.9rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
.reveal-head {
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #43331f;
|
||||||
|
border-bottom: 1px solid #b3a687;
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.reveal-row { display: flex; align-items: flex-start; gap: 0.6rem; margin-bottom: 0.5rem; }
|
||||||
|
.reveal-name {
|
||||||
|
font-family: "Courier Prime", monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #43331f;
|
||||||
|
min-width: 6.5rem;
|
||||||
|
padding-top: 0.4rem;
|
||||||
|
}
|
||||||
|
.reveal-name.reveal-winner { font-weight: 700; }
|
||||||
|
.reveal-cards { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
||||||
|
.reveal-cards :global(.card) { transform: scale(0.82); transform-origin: top left; margin: -0.35rem -0.8rem -1.1rem 0; }
|
||||||
|
.reveal-empty { font-family: "Caveat", cursive; color: #8a7a5e; padding-top: 0.5rem; }
|
||||||
|
|
||||||
.victory-notice { border-color: #a5842c; box-shadow: 0 0 0 4px rgba(201, 167, 42, 0.35), 0 18px 50px rgba(0, 0, 0, 0.6); }
|
.victory-notice { border-color: #a5842c; box-shadow: 0 0 0 4px rgba(201, 167, 42, 0.35), 0 18px 50px rgba(0, 0, 0, 0.6); }
|
||||||
.victory-trophy { font-size: 3rem; line-height: 1; }
|
.victory-trophy { font-size: 3rem; line-height: 1; }
|
||||||
.victory-name {
|
.victory-name {
|
||||||
|
|||||||
Reference in New Issue
Block a user