From 4528c66057ac5ca54996cc1837ee070cdd534899 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Mon, 17 Aug 2026 11:58:25 -0400 Subject: [PATCH] =?UTF-8?q?The=20peeked=20token=20joins=20its=20card=20?= =?UTF-8?q?=E2=80=94=20and=20stops=20hiding=20behind=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a board token now shows the thing itself, enlarged, above its card in the peek modal — whichever art is active, photograph or SVG. The art tables move to art.ts so the board renderer and the modal share one source. First cut overlapped: the card scales 2.1x out of its layout box, so flow neighbors sat underneath its visual footprint (the note had been compensating with a 6.8rem margin hack). A card-stage now reserves the card's TRUE visual size; token above, note below, nothing buried. Co-Authored-By: Claude Fable 5 --- packages/web/src/App.svelte | 38 +++++++++++++++++++++++++++++++---- packages/web/src/Board.svelte | 20 +----------------- packages/web/src/art.ts | 21 +++++++++++++++++++ 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 3073b58..4f0e782 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -8,7 +8,7 @@ import Replay from "./Replay.svelte"; import FxGallery from "./FxGallery.svelte"; import { fxForEvents, fxTtl, type BoardFx } from "./fx"; - import { tokenArt } from "./art"; + import { CREATURE_ART, objectArt, TERRAIN_ART, tokenArt } from "./art"; import { local } from "./local.svelte"; import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey, isMovableObject, sightedCellsFor, type GameView, eligibleCellsFor } from "@wizwar/engine"; import type { CardInstance, Side } from "@wizwar/engine"; @@ -63,6 +63,8 @@ let peekCard = $state(null); /** When the peeked card is a creature on the board, its live stats ride along. */ let peekCreatureId = $state(null); + /** The clicked token itself, enlarged beside its card. */ + let peekToken = $state(null); /** Bare-knuckle demolition: click a wall to punch it. */ let punchWallMode = $state(false); /** Leafing through the face-up discard pile. */ @@ -342,6 +344,7 @@ if (!view) return; peekCard = null; peekCreatureId = null; + peekToken = null; if (youMustRespond || youMustShield) { if (youMustRespond && card.cardId === "teleport") { selectedCard = card; // then click the escape square @@ -667,6 +670,13 @@ if (!cardId) return false; peekCard = { instanceId: `peek-${cardId}`, cardId }; peekCreatureId = creature?.id ?? null; + peekToken = + creature && CREATURE_ART[creature.kind] ? tokenArt(CREATURE_ART[creature.kind]!, "creatures") + : content && TERRAIN_ART[content.kind] ? tokenArt(TERRAIN_ART[content.kind]!, "terrain") + : objects.length > 0 && objectArt(cardId) ? tokenArt(objectArt(cardId)!, "objects") + : cardId === "dimensional-warp" ? tokenArt("dimensional-warp", "terrain") + : cardId === "boobytrap" ? tokenArt("boobytrap", "terrain") + : null; return true; } @@ -942,9 +952,14 @@ {#if peekCard || discardPeek}
{ peekCard = null; peekCreatureId = null; discardPeek = null; }} onkeydown={() => {}}> + onclick={() => { peekCard = null; peekCreatureId = null; peekToken = null; discardPeek = null; }} onkeydown={() => {}}>
- (faqCardId = id)} /> + {#if peekCard && peekToken} + + {/if} +
+ (faqCardId = id)} /> +
{#if peekNote}
{peekNote}
{/if}
@@ -2276,7 +2291,22 @@ } .big-peek :global(.card:hover) { transform: scale(2.1); } .big-peek { display: flex; flex-direction: column; align-items: center; } - .big-peek-note { margin-top: 6.8rem; max-width: 15rem; font-size: 0.8rem; } + .big-peek-note { margin-top: 0.7rem; max-width: 15rem; font-size: 0.8rem; } + /* The card scales 2.1x out of its layout box; the stage reserves its + TRUE visual footprint so neighbors never sit underneath it. */ + .card-stage { + width: calc(8.2rem * 2.1); + height: calc(11.4rem * 2.1); + display: grid; + place-items: center; + } + .big-peek-token { + width: 6.5rem; + height: 6.5rem; + object-fit: contain; + margin-bottom: 0.8rem; + filter: drop-shadow(2px 4px 8px rgba(0, 0, 0, 0.6)); + } .creature-dock { display: flex; flex-direction: column; diff --git a/packages/web/src/Board.svelte b/packages/web/src/Board.svelte index 11e08b6..34d94cc 100644 --- a/packages/web/src/Board.svelte +++ b/packages/web/src/Board.svelte @@ -2,7 +2,7 @@ import type { GameView } from "@wizwar/engine"; import type { Side } from "@wizwar/engine"; import { colorIndexOf as sharedColorIndex, wizardColor } from "./colors"; - import { tokenArt } from "./art"; + import { CREATURE_ART, objectArt, TERRAIN_ART, tokenArt } from "./art"; import TokenArt from "./TokenArt.svelte"; import { FX_SPRITES } from "./fx-sprites"; @@ -90,24 +90,6 @@ // 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; - const TERRAIN_ART: Record = { - stone: "solid-stone", thornbush: "thorn-bush", rosebush: "rosebush", - ooze: "killer-ooze", dust: "dustcloud", slime: "slime", - tacks: "tacks", pit: "pit", safe: "safe", - }; - const CREATURE_ART: Record = { - skeleton: "skeleton", troll: "troll", wraith: "wraith", - "fire-imp": "fire-imp", "democratic-monster": "democratic-monster", - shadow: "shadow", "alter-ego": "alter-ego", - }; - function objectArt(cardId: string): string | null { - if (cardId === "dagger") return "dagger"; - if (cardId === "large-rock") return "rock"; - if (cardId === "master-key") return "master-key"; - if (cardId.endsWith("stone")) return "magic-stone"; - if (cardId.endsWith("-wand")) return "magic-wand"; - return null; - } // Ongoing spells wear their look: ghostly when unseen, webbed when caught, // stone-gray under Medusa's gaze. function hasSpell(id: string, cardId: string): boolean { diff --git a/packages/web/src/art.ts b/packages/web/src/art.ts index 19b485e..590c3ef 100644 --- a/packages/web/src/art.ts +++ b/packages/web/src/art.ts @@ -15,3 +15,24 @@ const SVG_ART: Record = { export function tokenArt(file: string, cat: ArtCategory): string { return SVG_ART[cat] ? `/tokens-svg/${file}.svg` : `/tokens/${file}.png`; } + +// Which token file depicts each board thing (shared by the board renderer +// and the card-peek modal). Anything without an entry has no token art. +export const TERRAIN_ART: Record = { + stone: "solid-stone", thornbush: "thorn-bush", rosebush: "rosebush", + ooze: "killer-ooze", dust: "dustcloud", slime: "slime", + tacks: "tacks", pit: "pit", safe: "safe", +}; +export const CREATURE_ART: Record = { + skeleton: "skeleton", troll: "troll", wraith: "wraith", + "fire-imp": "fire-imp", "democratic-monster": "democratic-monster", + shadow: "shadow", "alter-ego": "alter-ego", +}; +export function objectArt(cardId: string): string | null { + if (cardId === "dagger") return "dagger"; + if (cardId === "large-rock") return "rock"; + if (cardId === "master-key") return "master-key"; + if (cardId.endsWith("stone")) return "magic-stone"; + if (cardId.endsWith("-wand")) return "magic-wand"; + return null; +}