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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-07 16:00:32 -05:00
co-authored by Claude Fable 5.1
parent 16fac5e6ca
commit 44c7ff8379
3 changed files with 30 additions and 3 deletions
+5
View File
@@ -60,6 +60,8 @@ export interface GameView {
/** Duration spells in play (public knowledge). */
sustained: SustainedEffect[];
squareContents: Record<string, SquareContent>;
/** Spells lodged in slime, oldest first (public: each cast was seen). */
slimeTraps: Record<string, { cardId: string; casterId: PlayerId }[]>;
/** Cells whose contents are glued down (public: the cast was seen). */
gluedCells: Record<string, true>;
/** 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(
+15 -3
View File
@@ -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")
+10
View File
@@ -373,6 +373,13 @@
{:else if content.kind === "safe"}
<rect x={sx * CELL + 10} y={sy * CELL + 12} width={CELL - 20} height={CELL - 22} rx="3" class="safe" />
{/if}
{#if content.kind === "slime" && (view.slimeTraps?.[key]?.length ?? 0) > 0}
<!-- spells waiting in the gel: everyone saw them cast -->
<g class="slime-charge" transform={`translate(${sx * CELL + CELL - 9}, ${sy * CELL + 9})`}>
<circle r="7" />
<text y="3.5">{view.slimeTraps[key]!.length}</text>
</g>
{/if}
{/each}
<!-- dimensional warp tokens -->
@@ -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; }