The discard pile lies face-up, and the chronicle names what falls
At the table the discards sit face-up for anyone to leaf through. The view now carries the pile (deck order stays secret), the "discards N" count in the rail became a link, and it opens a booklet-styled reader: every cast-away card newest first, tap any to enlarge. And the chronicle stops mumbling "discards 1 card(s)" — it names names, so a wraith's theft reads "bob discards Fireball," exactly what a table would see as the card hits the pile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d1eace9530
commit
0ebae03b1c
@@ -47,6 +47,8 @@ export interface GameView {
|
|||||||
treasures: TreasureState[];
|
treasures: TreasureState[];
|
||||||
deckCount: number;
|
deckCount: number;
|
||||||
discardCount: number;
|
discardCount: number;
|
||||||
|
/** The discard pile, face-up as at the table (last = most recent). */
|
||||||
|
discardPile: CardInstance[];
|
||||||
/** Cards on the stack are face-up: the whole exchange is public. */
|
/** Cards on the stack are face-up: the whole exchange is public. */
|
||||||
stack: CastStack | null;
|
stack: CastStack | null;
|
||||||
pendingDiscard: PlayerId | null;
|
pendingDiscard: PlayerId | null;
|
||||||
@@ -121,6 +123,7 @@ export function viewFor(state: GameState, playerId: PlayerId): GameView {
|
|||||||
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,
|
||||||
|
discardPile: [...state.discard],
|
||||||
stack: state.stack,
|
stack: state.stack,
|
||||||
pendingDiscard: state.pendingDiscard,
|
pendingDiscard: state.pendingDiscard,
|
||||||
sustained: state.sustained.map((s) => ({ ...s })),
|
sustained: state.sustained.map((s) => ({ ...s })),
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
let peekCreatureId = $state<string | null>(null);
|
let peekCreatureId = $state<string | null>(null);
|
||||||
/** Bare-knuckle demolition: click a wall to punch it. */
|
/** Bare-knuckle demolition: click a wall to punch it. */
|
||||||
let punchWallMode = $state(false);
|
let punchWallMode = $state(false);
|
||||||
|
/** Leafing through the face-up discard pile. */
|
||||||
|
let showDiscards = $state(false);
|
||||||
let helpTab = $state<"play" | "rules" | "cards" | "about" | "tally">("play");
|
let helpTab = $state<"play" | "rules" | "cards" | "about" | "tally">("play");
|
||||||
let hotseatCount = $state(2);
|
let hotseatCount = $state(2);
|
||||||
let setupName = $state("");
|
let setupName = $state("");
|
||||||
@@ -714,6 +716,26 @@
|
|||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{#if showDiscards && view}
|
||||||
|
<div class="scrim attack-scrim" role="button" tabindex="-1" onclick={() => (showDiscards = false)} onkeydown={() => {}}>
|
||||||
|
<div class="discard-book" role="dialog" aria-label="the discard pile" tabindex="-1"
|
||||||
|
onclick={(e) => e.stopPropagation()} onkeydown={() => {}}>
|
||||||
|
<header class="discard-head">
|
||||||
|
<span>The discard pile — {view.discardCount} card{view.discardCount === 1 ? "" : "s"}, newest first</span>
|
||||||
|
<button class="inspector-close discard-close" onclick={() => (showDiscards = false)} aria-label="close">×</button>
|
||||||
|
</header>
|
||||||
|
<div class="discard-grid">
|
||||||
|
{#each [...view.discardPile].reverse() as card (card.instanceId)}
|
||||||
|
<Card {card} onclick={() => (peekCard = card)} />
|
||||||
|
{/each}
|
||||||
|
{#if view.discardPile.length === 0}
|
||||||
|
<span class="discard-empty">Nothing has been cast away yet.</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if peekCard}
|
{#if peekCard}
|
||||||
<div class="inspector" role="img" aria-label="displayed card, enlarged">
|
<div class="inspector" role="img" aria-label="displayed card, enlarged">
|
||||||
<button class="inspector-close" onclick={() => { peekCard = null; peekCreatureId = null; }} aria-label="hide card">×</button>
|
<button class="inspector-close" onclick={() => { peekCard = null; peekCreatureId = null; }} aria-label="hide card">×</button>
|
||||||
@@ -1095,7 +1117,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<div class="deck-line">draw pile {view.deckCount} · discards {view.discardCount}</div>
|
<div class="deck-line">draw pile {view.deckCount} ·
|
||||||
|
<button class="discard-link" onclick={() => (showDiscards = true)}>discards {view.discardCount}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chronicle" aria-label="game log" bind:this={chronicleEl}>
|
<div class="chronicle" aria-label="game log" bind:this={chronicleEl}>
|
||||||
@@ -1837,6 +1861,51 @@
|
|||||||
.attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; }
|
.attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; }
|
||||||
.attack-fist { font-size: 1.1rem; color: #43331f; }
|
.attack-fist { font-size: 1.1rem; color: #43331f; }
|
||||||
|
|
||||||
|
.discard-link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.discard-link:hover { color: #43331f; }
|
||||||
|
.discard-book {
|
||||||
|
background: #efe8d4;
|
||||||
|
border: 1px solid #b3a687;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
|
||||||
|
width: min(52rem, 94vw);
|
||||||
|
max-height: calc(100vh - 4rem);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.discard-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-bottom: 2px solid #43331f;
|
||||||
|
font-family: "Oswald", sans-serif;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #43331f;
|
||||||
|
}
|
||||||
|
.discard-close { position: static; }
|
||||||
|
.discard-grid {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.9rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(8.4rem, 1fr));
|
||||||
|
gap: 0.7rem;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
.discard-empty { font-family: "Caveat", cursive; color: #8a7a5e; font-size: 1.1rem; }
|
||||||
|
|
||||||
.peek-note {
|
.peek-note {
|
||||||
margin-top: 0.4rem;
|
margin-top: 0.4rem;
|
||||||
background: #2b2218;
|
background: #2b2218;
|
||||||
|
|||||||
@@ -151,7 +151,8 @@ export function humanize(e: GameEvent): string | null {
|
|||||||
}
|
}
|
||||||
case "treasureDropped": return e.onHomeOf ? `${e.player} drops a treasure on ${e.onHomeOf}'s home base!` : `${e.player} drops a treasure.`;
|
case "treasureDropped": return e.onHomeOf ? `${e.player} drops a treasure on ${e.onHomeOf}'s home base!` : `${e.player} drops a treasure.`;
|
||||||
case "playerEliminated": return e.reason === "treasuresLost" ? `${e.player} is eliminated — both treasures lost!` : null;
|
case "playerEliminated": return e.reason === "treasuresLost" ? `${e.player} is eliminated — both treasures lost!` : null;
|
||||||
case "cardsDiscarded": return `${e.player} discards ${e.cards.length} card(s).`;
|
case "cardsDiscarded":
|
||||||
|
return `${e.player} discards ${e.cards.map((c) => spellName(c.cardId)).join(", ")}.`;
|
||||||
case "cardsDrawn": return e.count > 0 ? `${e.player} draws ${e.count} card(s).` : null;
|
case "cardsDrawn": return e.count > 0 ? `${e.player} draws ${e.count} card(s).` : null;
|
||||||
case "deckReshuffled": return `The discard pile is reshuffled (${e.size} cards).`;
|
case "deckReshuffled": return `The discard pile is reshuffled (${e.size} cards).`;
|
||||||
case "turnEnded": return null;
|
case "turnEnded": return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user