From 97a3305d0792cf4d6f3830c6f1f281488f3a271c Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 16 Aug 2026 10:39:56 -0400 Subject: [PATCH] Every token on the board can be read as its card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "What does a rosebush do again?" now has an in-game answer: press and hold any square (450ms) to open the card behind whatever occupies it — creature, terrain, dropped object, warp token, or boobytrap marker — in the view-only inspector corner. A plain click that claims no game action peeks too: out of turn, every tap reads; on your turn, taps beyond your reach read instead of doing nothing, and clicking an opponent's creature with nothing selected reads its card. Priority when a square holds several stories: creature, then terrain, then the topmost dropped object. Co-Authored-By: Claude Fable 5 --- packages/web/src/App.svelte | 37 +++++++++++++++++++++++++++++++++-- packages/web/src/Board.svelte | 25 ++++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 04d984f..7465edf 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -267,7 +267,8 @@ } function clickCell(cell: { x: number; y: number }) { - if (!view || !isYourTurn) return; + if (!view) return; + if (!isYourTurn) { peekAt(cell); return; } if (pendingCellFor && selectedCard) { // Stage 2 of teleport-opponent: destination chosen. dispatch({ @@ -371,6 +372,34 @@ return; } } + // The click claimed no action — offer the square's card instead. + peekAt(cell); + } + + /** Card ids behind the terrain tokens, for tap-to-read. */ + const CONTENT_CARD: Record = { + stone: "fill-square-with-stone", thornbush: "thornbush", rosebush: "rosebush", + ooze: "killer-ooze", dust: "dust-cloud", slime: "fill-square-with-slime", + tacks: "handful-of-tacks", pit: "create-pit", safe: "safe", + }; + + /** Show the card behind whatever occupies this square (view-only). */ + function peekAt(cell: { x: number; y: number }): boolean { + if (!view) return false; + const key = `${cell.x},${cell.y}`; + const creature = view.creatures.find((c) => c.position.x === cell.x && c.position.y === cell.y); + const content = view.squareContents[key]; + const objects = view.groundObjects[key] ?? []; + const cardId = + creature ? creature.kind + : content ? CONTENT_CARD[content.kind] + : objects.length > 0 ? objects[objects.length - 1]!.cardId + : view.dimWarps.some((w) => (w.a.x === cell.x && w.a.y === cell.y) || (w.b.x === cell.x && w.b.y === cell.y)) ? "dimensional-warp" + : view.boobytraps.some((t) => t.cells.some((c) => c.x === cell.x && c.y === cell.y)) ? "boobytrap" + : null; + if (!cardId) return false; + peekCard = { instanceId: `peek-${cardId}`, cardId }; + return true; } function clickWarp(cell: { x: number; y: number }, side: Side) { @@ -395,9 +424,10 @@ } function clickCreature(creatureId: string) { - if (!view || !isYourTurn) return; + if (!view) return; const creature = view.creatures.find((c) => c.id === creatureId); if (!creature) return; + if (!isYourTurn) { peekAt(creature.position); return; } if (selectedCard && (CREATURE_TARGET_CARDS.has(selectedCard.cardId) || cardDef(selectedCard.cardId).cardType === "attack")) { dispatch({ type: "cast", instanceId: selectedCard.instanceId, @@ -416,6 +446,8 @@ const mine = creature.controllerId === view.you || creature.kind === "democratic-monster"; if (mine) { selectedCreature = selectedCreature === creatureId ? null : creatureId; + } else { + peekAt(creature.position); } } @@ -845,6 +877,7 @@ onCreatureClick={clickCreature} onWarpClick={clickWarp} markedCell={tradeFrom ?? pendingSectorFrom} + onCellPeek={(c) => peekAt(c)} {litCells} /> diff --git a/packages/web/src/Board.svelte b/packages/web/src/Board.svelte index 62aab6c..0a875c6 100644 --- a/packages/web/src/Board.svelte +++ b/packages/web/src/Board.svelte @@ -14,6 +14,7 @@ onPlayerClick, onCreatureClick, onWarpClick, + onCellPeek, markedCell = null, litCells = null, }: { @@ -25,6 +26,8 @@ onPlayerClick?: (playerId: string) => void; onCreatureClick?: (creatureId: string) => void; onWarpClick?: (cell: { x: number; y: number }, side: Side) => void; + /** Long-press on a square: read the card behind whatever occupies it. */ + onCellPeek?: (cell: { x: number; y: number }) => void; /** First square of a two-square spell: marked so the click reads as taken. */ markedCell?: { x: number; y: number } | null; /** When set, squares NOT in this set dim — the targeting aid. */ @@ -33,6 +36,23 @@ const PLAYER_COLORS = ["#1a9c46", "#d3352b", "#c9308f", "#3a3ac0", "#2ab0c9", "#c9a72a"]; + // Press-and-hold peeks the square's card; a fired hold swallows the click. + let peekTimer: ReturnType | null = null; + let peekFired = false; + function pressCell(c: { x: number; y: number }) { + peekFired = false; + if (!onCellPeek) return; + peekTimer = setTimeout(() => { peekFired = true; onCellPeek(c); }, 450); + } + function releasePress() { + if (peekTimer) clearTimeout(peekTimer); + peekTimer = null; + } + function tapCell(c: { x: number; y: number }) { + if (peekFired) { peekFired = false; return; } + onCellClick?.(c); + } + // Real token art, cropped from the owner's physical set (public/tokens/). // Anything without an entry falls back to the vector rendering below. const USE_TOKEN_ART = true; @@ -137,7 +157,10 @@ x={c.x * CELL} y={c.y * CELL} width={CELL} height={CELL} class="floor" role="button" tabindex="-1" - onclick={() => onCellClick?.(c)} + onclick={() => tapCell(c)} + onpointerdown={() => pressCell(c)} + onpointerup={releasePress} + onpointerleave={releasePress} onkeydown={() => {}} /> {/each}