From 0ebae03b1c3123106d634e65277690bb573b130a Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 16 Aug 2026 13:28:55 -0400 Subject: [PATCH] The discard pile lies face-up, and the chronicle names what falls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/engine/src/view.ts | 3 ++ packages/web/src/App.svelte | 71 +++++++++++++++++++++++++++++++++- packages/web/src/net.svelte.ts | 3 +- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/view.ts b/packages/engine/src/view.ts index 7d9626f..f17ec72 100644 --- a/packages/engine/src/view.ts +++ b/packages/engine/src/view.ts @@ -47,6 +47,8 @@ export interface GameView { treasures: TreasureState[]; deckCount: 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. */ stack: CastStack | null; pendingDiscard: PlayerId | null; @@ -121,6 +123,7 @@ export function viewFor(state: GameState, playerId: PlayerId): GameView { treasures: state.treasures.map((t) => ({ ...t })), deckCount: state.deck.length, discardCount: state.discard.length, + discardPile: [...state.discard], stack: state.stack, pendingDiscard: state.pendingDiscard, sustained: state.sustained.map((s) => ({ ...s })), diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index af658a3..1850b69 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -26,6 +26,8 @@ let peekCreatureId = $state(null); /** Bare-knuckle demolition: click a wall to punch it. */ 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 hotseatCount = $state(2); let setupName = $state(""); @@ -714,6 +716,26 @@ + {#if showDiscards && view} +
(showDiscards = false)} onkeydown={() => {}}> + +
+ {/if} + {#if peekCard} {/if} {/each} -
draw pile {view.deckCount} · discards {view.discardCount}
+
draw pile {view.deckCount} · + +
@@ -1837,6 +1861,51 @@ .attack-power { font-family: "Courier Prime", monospace; color: #6b5a41; font-size: 0.9rem; } .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 { margin-top: 0.4rem; background: #2b2218; diff --git a/packages/web/src/net.svelte.ts b/packages/web/src/net.svelte.ts index fb29c96..5c1868c 100644 --- a/packages/web/src/net.svelte.ts +++ b/packages/web/src/net.svelte.ts @@ -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 "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 "deckReshuffled": return `The discard pile is reshuffled (${e.size} cards).`; case "turnEnded": return null;