The real 1993 token art takes the board
All 45 tokens cropped from Eric's photos of the physical counter sheets by connected-component segmentation (with cable/reflection filtering, merged-blob rescue, and one hand-tuned crop for the red treasure the charging cable was hugging). Terrain squares, monsters, wizards, treasures, floor objects, and dimensional warps now render the genuine hand-inked art: solid stone, thornbush, rosebush, killer ooze, dustcloud, slime, tacks, pit, safe, skeleton, troll, wraith, fire imp, democratic monster, shadow, alter ego, dagger, rock, master key, magic stones and wands, the six wizard standees, and the six color-keyed treasure chests. Wizards and creatures wear a thin controller-color ring; art tokens cast a soft cardboard shadow; terrain art passes clicks through to the floor while wizards and creatures stay clickable. Every vector rendering remains in place as automatic fallback (USE_TOKEN_ART flag plus per-kind mapping), per Eric's request. Sprites live in packages/web/public/tokens at 144px. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 61 KiB |
@@ -27,6 +27,36 @@
|
||||
|
||||
const PLAYER_COLORS = ["#1a9c46", "#d3352b", "#c9308f", "#3a3ac0", "#2ab0c9", "#c9a72a"];
|
||||
|
||||
// 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<string, string> = {
|
||||
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<string, string> = {
|
||||
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;
|
||||
}
|
||||
function wizardArt(id: string): string {
|
||||
const idx = view.players.findIndex((p) => p.id === id);
|
||||
return `/tokens/wizard-${((idx % 6) + 6) % 6}.png`;
|
||||
}
|
||||
function treasureArt(owner: string): string {
|
||||
const idx = view.players.findIndex((p) => p.id === owner);
|
||||
return `/tokens/treasure-${((idx % 6) + 6) % 6}.png`;
|
||||
}
|
||||
|
||||
function playerColor(id: string): string {
|
||||
const idx = view.players.findIndex((p) => p.id === id);
|
||||
return PLAYER_COLORS[idx % PLAYER_COLORS.length]!;
|
||||
@@ -124,19 +154,37 @@
|
||||
{#if t.position}
|
||||
{@const tx = t.position.x * CELL + CELL / 2}
|
||||
{@const ty = t.position.y * CELL + CELL * 0.72}
|
||||
{#if USE_TOKEN_ART}
|
||||
<image
|
||||
href={treasureArt(t.owner)}
|
||||
x={tx - CELL * 0.26} y={ty - CELL * 0.26}
|
||||
width={CELL * 0.52} height={CELL * 0.52}
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
class="token-art small"
|
||||
/>
|
||||
{:else}
|
||||
<g class="treasure-g">
|
||||
<circle cx={tx} cy={ty} r={8} class="treasure" fill={playerColor(t.owner)} />
|
||||
<circle cx={tx} cy={ty} r={4.5} class="treasure-swirl" />
|
||||
<circle cx={tx} cy={ty} r={1.6} class="treasure-core" />
|
||||
</g>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<!-- terrain: solid stone and thornbushes -->
|
||||
{#each Object.entries(view.squareContents) as [key, content] (key)}
|
||||
{@const sx = Number(key.split(",")[0])}
|
||||
{@const sy = Number(key.split(",")[1])}
|
||||
{#if content.kind === "stone"}
|
||||
{#if USE_TOKEN_ART && TERRAIN_ART[content.kind]}
|
||||
<image
|
||||
href={`/tokens/${TERRAIN_ART[content.kind]}.png`}
|
||||
x={sx * CELL + 3} y={sy * CELL + 3}
|
||||
width={CELL - 6} height={CELL - 6}
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
class="token-art"
|
||||
/>
|
||||
{:else if content.kind === "stone"}
|
||||
<rect x={sx * CELL + 2} y={sy * CELL + 2} width={CELL - 4} height={CELL - 4} class="stone" rx="4" />
|
||||
{:else if content.kind === "thornbush" || content.kind === "rosebush"}
|
||||
<circle cx={sx * CELL + CELL / 2} cy={sy * CELL + CELL / 2} r={CELL * 0.36}
|
||||
@@ -158,8 +206,17 @@
|
||||
<!-- dimensional warp tokens -->
|
||||
{#each view.dimWarps as w, wi (wi)}
|
||||
{#each [w.a, w.b] as tok, i (i)}
|
||||
{#if USE_TOKEN_ART}
|
||||
<image
|
||||
href="/tokens/dimensional-warp.png"
|
||||
x={tok.x * CELL + CELL * 0.16} y={tok.y * CELL + CELL * 0.16}
|
||||
width={CELL * 0.68} height={CELL * 0.68}
|
||||
preserveAspectRatio="xMidYMid slice" class="token-art"
|
||||
/>
|
||||
{:else}
|
||||
<circle cx={tok.x * CELL + CELL * 0.5} cy={tok.y * CELL + CELL * 0.5} r={CELL * 0.3}
|
||||
class="dimwarp" />
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
@@ -176,12 +233,25 @@
|
||||
{@const gx = Number(key.split(",")[0])}
|
||||
{@const gy = Number(key.split(",")[1])}
|
||||
{#each objects as o, i (o.instanceId)}
|
||||
{@const art = USE_TOKEN_ART ? objectArt(o.cardId) : null}
|
||||
{#if art}
|
||||
<image
|
||||
href={`/tokens/${art}.png`}
|
||||
x={gx * CELL + 4 + i * 9} y={gy * CELL + CELL - CELL * 0.42 - 3}
|
||||
width={CELL * 0.4} height={CELL * 0.4}
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
class="token-art small"
|
||||
>
|
||||
<title>{o.cardId}</title>
|
||||
</image>
|
||||
{:else}
|
||||
<rect
|
||||
x={gx * CELL + 6 + i * 8} y={gy * CELL + CELL - 16}
|
||||
width={12} height={10} rx="2" class="ground-object"
|
||||
>
|
||||
<title>{o.cardId}</title>
|
||||
</rect>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
@@ -259,10 +329,25 @@
|
||||
onkeydown={() => {}}
|
||||
class="wizard"
|
||||
>
|
||||
{#if USE_TOKEN_ART}
|
||||
<image
|
||||
href={wizardArt(p.id)}
|
||||
x={cx - CELL * 0.3} y={cy - CELL * 0.3}
|
||||
width={CELL * 0.6} height={CELL * 0.6}
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
class="token-art hit"
|
||||
/>
|
||||
<rect
|
||||
x={cx - CELL * 0.3} y={cy - CELL * 0.3}
|
||||
width={CELL * 0.6} height={CELL * 0.6}
|
||||
class="wizard-ring" stroke={playerColor(p.id)}
|
||||
/>
|
||||
{:else}
|
||||
<circle {cx} {cy} r={12} fill={playerColor(p.id)} stroke="#2b2218" stroke-width="2" />
|
||||
<text x={cx} y={cy + 4} class="wizard-label">{p.id[0]?.toUpperCase()}</text>
|
||||
{/if}
|
||||
{#if p.carriedTreasureId}
|
||||
<circle cx={cx + 9} cy={cy + 9} r={5} class="carried" />
|
||||
<circle cx={cx + CELL * 0.26} cy={cy + CELL * 0.26} r={5} class="carried" />
|
||||
{/if}
|
||||
</g>
|
||||
{/each}
|
||||
@@ -277,6 +362,25 @@
|
||||
onclick={(ev) => { ev.stopPropagation(); onCreatureClick?.(c.id); }}
|
||||
onkeydown={() => {}}
|
||||
>
|
||||
{#if USE_TOKEN_ART && CREATURE_ART[c.kind]}
|
||||
<image
|
||||
href={`/tokens/${CREATURE_ART[c.kind]}.png`}
|
||||
x={ccx - CELL * 0.26} y={ccy - CELL * 0.26}
|
||||
width={CELL * 0.52} height={CELL * 0.52}
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
class="token-art hit"
|
||||
class:selected-art={c.id === selectedCreatureId}
|
||||
>
|
||||
<title>{c.kind} ({c.controllerId}) — {c.damage}/{Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg</title>
|
||||
</image>
|
||||
<rect
|
||||
x={ccx - CELL * 0.26} y={ccy - CELL * 0.26}
|
||||
width={CELL * 0.52} height={CELL * 0.52}
|
||||
class="creature-ring"
|
||||
class:selected={c.id === selectedCreatureId}
|
||||
stroke={playerColor(c.controllerId)}
|
||||
/>
|
||||
{:else}
|
||||
<rect
|
||||
x={ccx - 10} y={ccy - 10} width={20} height={20} rx="3"
|
||||
transform={`rotate(45 ${ccx} ${ccy})`}
|
||||
@@ -287,6 +391,7 @@
|
||||
<title>{c.kind} ({c.controllerId}) — {c.damage}/{Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg</title>
|
||||
</rect>
|
||||
<text x={ccx} y={ccy + 4} class="creature-label">{c.kind === "fire-imp" ? "I" : c.kind === "democratic-monster" ? "D" : c.kind[0]?.toUpperCase()}</text>
|
||||
{/if}
|
||||
</g>
|
||||
{/each}
|
||||
|
||||
@@ -349,6 +454,17 @@
|
||||
.trap-token { fill: #513c22; stroke: #201709; stroke-width: 1.5; }
|
||||
.trap-real { stroke: #d3352b; stroke-width: 2.5; }
|
||||
.dimwarp { fill: none; stroke: #5b3f9e; stroke-width: 3.5; stroke-dasharray: 4 3; }
|
||||
.token-art {
|
||||
filter: drop-shadow(0.5px 1.5px 1.5px rgba(10, 8, 4, 0.5));
|
||||
pointer-events: none;
|
||||
}
|
||||
.token-art.hit { pointer-events: auto; cursor: pointer; }
|
||||
.creature-ring, .wizard-ring {
|
||||
fill: none;
|
||||
stroke-width: 2.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.creature-ring.selected { stroke-width: 4; stroke-dasharray: 5 3; }
|
||||
.ground-object { fill: #a6812e; stroke: #4a3a10; stroke-width: 1; }
|
||||
.illusion { stroke: #7a6f9a; stroke-width: 4; stroke-dasharray: 6 5; opacity: 0.7; }
|
||||
.creature { cursor: pointer; }
|
||||
|
||||