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"];
|
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 {
|
function playerColor(id: string): string {
|
||||||
const idx = view.players.findIndex((p) => p.id === id);
|
const idx = view.players.findIndex((p) => p.id === id);
|
||||||
return PLAYER_COLORS[idx % PLAYER_COLORS.length]!;
|
return PLAYER_COLORS[idx % PLAYER_COLORS.length]!;
|
||||||
@@ -124,11 +154,21 @@
|
|||||||
{#if t.position}
|
{#if t.position}
|
||||||
{@const tx = t.position.x * CELL + CELL / 2}
|
{@const tx = t.position.x * CELL + CELL / 2}
|
||||||
{@const ty = t.position.y * CELL + CELL * 0.72}
|
{@const ty = t.position.y * CELL + CELL * 0.72}
|
||||||
<g class="treasure-g">
|
{#if USE_TOKEN_ART}
|
||||||
<circle cx={tx} cy={ty} r={8} class="treasure" fill={playerColor(t.owner)} />
|
<image
|
||||||
<circle cx={tx} cy={ty} r={4.5} class="treasure-swirl" />
|
href={treasureArt(t.owner)}
|
||||||
<circle cx={tx} cy={ty} r={1.6} class="treasure-core" />
|
x={tx - CELL * 0.26} y={ty - CELL * 0.26}
|
||||||
</g>
|
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}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
@@ -136,7 +176,15 @@
|
|||||||
{#each Object.entries(view.squareContents) as [key, content] (key)}
|
{#each Object.entries(view.squareContents) as [key, content] (key)}
|
||||||
{@const sx = Number(key.split(",")[0])}
|
{@const sx = Number(key.split(",")[0])}
|
||||||
{@const sy = Number(key.split(",")[1])}
|
{@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" />
|
<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"}
|
{:else if content.kind === "thornbush" || content.kind === "rosebush"}
|
||||||
<circle cx={sx * CELL + CELL / 2} cy={sy * CELL + CELL / 2} r={CELL * 0.36}
|
<circle cx={sx * CELL + CELL / 2} cy={sy * CELL + CELL / 2} r={CELL * 0.36}
|
||||||
@@ -158,8 +206,17 @@
|
|||||||
<!-- dimensional warp tokens -->
|
<!-- dimensional warp tokens -->
|
||||||
{#each view.dimWarps as w, wi (wi)}
|
{#each view.dimWarps as w, wi (wi)}
|
||||||
{#each [w.a, w.b] as tok, i (i)}
|
{#each [w.a, w.b] as tok, i (i)}
|
||||||
<circle cx={tok.x * CELL + CELL * 0.5} cy={tok.y * CELL + CELL * 0.5} r={CELL * 0.3}
|
{#if USE_TOKEN_ART}
|
||||||
class="dimwarp" />
|
<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}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
@@ -176,12 +233,25 @@
|
|||||||
{@const gx = Number(key.split(",")[0])}
|
{@const gx = Number(key.split(",")[0])}
|
||||||
{@const gy = Number(key.split(",")[1])}
|
{@const gy = Number(key.split(",")[1])}
|
||||||
{#each objects as o, i (o.instanceId)}
|
{#each objects as o, i (o.instanceId)}
|
||||||
<rect
|
{@const art = USE_TOKEN_ART ? objectArt(o.cardId) : null}
|
||||||
x={gx * CELL + 6 + i * 8} y={gy * CELL + CELL - 16}
|
{#if art}
|
||||||
width={12} height={10} rx="2" class="ground-object"
|
<image
|
||||||
>
|
href={`/tokens/${art}.png`}
|
||||||
<title>{o.cardId}</title>
|
x={gx * CELL + 4 + i * 9} y={gy * CELL + CELL - CELL * 0.42 - 3}
|
||||||
</rect>
|
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}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
@@ -259,10 +329,25 @@
|
|||||||
onkeydown={() => {}}
|
onkeydown={() => {}}
|
||||||
class="wizard"
|
class="wizard"
|
||||||
>
|
>
|
||||||
<circle {cx} {cy} r={12} fill={playerColor(p.id)} stroke="#2b2218" stroke-width="2" />
|
{#if USE_TOKEN_ART}
|
||||||
<text x={cx} y={cy + 4} class="wizard-label">{p.id[0]?.toUpperCase()}</text>
|
<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}
|
{#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}
|
{/if}
|
||||||
</g>
|
</g>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -277,16 +362,36 @@
|
|||||||
onclick={(ev) => { ev.stopPropagation(); onCreatureClick?.(c.id); }}
|
onclick={(ev) => { ev.stopPropagation(); onCreatureClick?.(c.id); }}
|
||||||
onkeydown={() => {}}
|
onkeydown={() => {}}
|
||||||
>
|
>
|
||||||
<rect
|
{#if USE_TOKEN_ART && CREATURE_ART[c.kind]}
|
||||||
x={ccx - 10} y={ccy - 10} width={20} height={20} rx="3"
|
<image
|
||||||
transform={`rotate(45 ${ccx} ${ccy})`}
|
href={`/tokens/${CREATURE_ART[c.kind]}.png`}
|
||||||
class="creature-body"
|
x={ccx - CELL * 0.26} y={ccy - CELL * 0.26}
|
||||||
class:selected={c.id === selectedCreatureId}
|
width={CELL * 0.52} height={CELL * 0.52}
|
||||||
stroke={playerColor(c.controllerId)}
|
preserveAspectRatio="xMidYMid slice"
|
||||||
>
|
class="token-art hit"
|
||||||
<title>{c.kind} ({c.controllerId}) — {c.damage}/{Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg</title>
|
class:selected-art={c.id === selectedCreatureId}
|
||||||
</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>
|
<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})`}
|
||||||
|
class="creature-body"
|
||||||
|
class:selected={c.id === selectedCreatureId}
|
||||||
|
stroke={playerColor(c.controllerId)}
|
||||||
|
>
|
||||||
|
<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>
|
</g>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
@@ -349,6 +454,17 @@
|
|||||||
.trap-token { fill: #513c22; stroke: #201709; stroke-width: 1.5; }
|
.trap-token { fill: #513c22; stroke: #201709; stroke-width: 1.5; }
|
||||||
.trap-real { stroke: #d3352b; stroke-width: 2.5; }
|
.trap-real { stroke: #d3352b; stroke-width: 2.5; }
|
||||||
.dimwarp { fill: none; stroke: #5b3f9e; stroke-width: 3.5; stroke-dasharray: 4 3; }
|
.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; }
|
.ground-object { fill: #a6812e; stroke: #4a3a10; stroke-width: 1; }
|
||||||
.illusion { stroke: #7a6f9a; stroke-width: 4; stroke-dasharray: 6 5; opacity: 0.7; }
|
.illusion { stroke: #7a6f9a; stroke-width: 4; stroke-dasharray: 6 5; opacity: 0.7; }
|
||||||
.creature { cursor: pointer; }
|
.creature { cursor: pointer; }
|
||||||
|
|||||||