From 44c7ff83798e4d9ef5b53e108dc229e5fec8d2bf Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Mon, 7 Sep 2026 16:00:32 -0500 Subject: [PATCH] A slime shows how many spells wait in it, and a peek names them Every cast into the gel was seen at the table, so the count is public: a badge on the slime, and the peek lists each spell with its caster. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG --- packages/engine/src/view.ts | 5 +++++ packages/web/src/App.svelte | 18 +++++++++++++++--- packages/web/src/Board.svelte | 10 ++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/engine/src/view.ts b/packages/engine/src/view.ts index e8c14dc..6e6d9d4 100644 --- a/packages/engine/src/view.ts +++ b/packages/engine/src/view.ts @@ -60,6 +60,8 @@ export interface GameView { /** Duration spells in play (public knowledge). */ sustained: SustainedEffect[]; squareContents: Record; + /** Spells lodged in slime, oldest first (public: each cast was seen). */ + slimeTraps: Record; /** Cells whose contents are glued down (public: the cast was seen). */ gluedCells: Record; /** Safes standing open (their combination entered this turn). */ @@ -156,6 +158,9 @@ export function viewFor(state: GameState, playerId: PlayerId): GameView { pendingDiscard: state.pendingDiscard, sustained: state.sustained.map((s) => ({ ...s })), squareContents: { ...state.squareContents }, + slimeTraps: Object.fromEntries( + Object.entries(state.slimeTraps).map(([k, traps]) => [k, traps.map((t) => ({ cardId: t.card.cardId, casterId: t.casterId }))]), + ), gluedCells: { ...state.gluedCells }, openSafes: [...state.openSafes], groundObjects: Object.fromEntries( diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index da90097..efbaad8 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1003,10 +1003,21 @@ const atk = c.justCreated ? "no attack the turn it appears" : c.attackUsed ? "attack spent" : "attack ready"; return `${hp} · ${moves} · ${atk}`; } + /** The square a peek is looking at, for notes the card alone cannot carry. */ + let peekCell = $state<{ x: number; y: number } | null>(null); const peekNote = $derived.by(() => { - if (!view || !peekCreatureId) return null; - const c = view.creatures.find((c) => c.id === peekCreatureId); - return c ? creatureStats(c) : null; + if (!view) return null; + if (peekCreatureId) { + const c = view.creatures.find((c) => c.id === peekCreatureId); + return c ? creatureStats(c) : null; + } + if (peekCell && view.squareContents[cellKey(peekCell)]?.kind === "slime") { + const waiting = view.slimeTraps?.[cellKey(peekCell)] ?? []; + if (waiting.length > 0) { + return `Waiting in the gel for the next wizard in: ${waiting.map((t) => `${cardDef(t.cardId).name} (${t.casterId})`).join(", ")}`; + } + } + return null; }); /** Show the card behind whatever occupies this square (view-only). */ @@ -1026,6 +1037,7 @@ if (!cardId) return false; peekCard = { instanceId: `peek-${cardId}`, cardId }; peekCreatureId = creature?.id ?? null; + peekCell = { ...cell }; peekToken = creature && CREATURE_ART[creature.kind] ? tokenArt(CREATURE_ART[creature.kind]!, "creatures") : content && TERRAIN_ART[content.kind] ? tokenArt(TERRAIN_ART[content.kind]!, "terrain") diff --git a/packages/web/src/Board.svelte b/packages/web/src/Board.svelte index df6107d..d90de3d 100644 --- a/packages/web/src/Board.svelte +++ b/packages/web/src/Board.svelte @@ -373,6 +373,13 @@ {:else if content.kind === "safe"} {/if} + {#if content.kind === "slime" && (view.slimeTraps?.[key]?.length ?? 0) > 0} + + + + {view.slimeTraps[key]!.length} + + {/if} {/each} @@ -765,6 +772,9 @@ .rose { fill: #2e7d32; stroke: #b0245a; stroke-width: 3; } .ooze { fill: #58a12b; opacity: 0.85; } .slime { fill: #8ec52e; opacity: 0.85; } + .slime-charge { pointer-events: none; } + .slime-charge circle { fill: #3b2a6b; stroke: #f2cf5b; stroke-width: 1.5; } + .slime-charge text { fill: #f7ecc8; font-size: 10px; font-weight: 700; text-anchor: middle; font-family: "Oswald", sans-serif; } .dust { fill: #9b9184; opacity: 0.75; } .pit { fill: #171512; } .tacks { font-size: 15px; text-anchor: middle; fill: #444; }