The art workshop's side door

The hand-drawn SVG token set wires in behind deliberately
undocumented query params — svgPlayers, svgCreatures, svgTerrain,
svgObjects, each =true — swapping that category's art everywhere it
draws: board tokens, lobby standees, roster. public/tokens-svg
symlinks research/tokens-svg, so under the dev server an edit to the
source art shows on the next refresh, and the production build copies
real files through the link. Nobody finds the door unless handed the
key; the photographed set remains the face of the game.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-17 11:27:28 -04:00
co-authored by Claude Fable 5
parent 93bdbb403a
commit e0e96ca5ca
50 changed files with 506 additions and 9 deletions
+1
View File
@@ -0,0 +1 @@
../../../research/tokens-svg
+4 -3
View File
@@ -8,6 +8,7 @@
import Replay from "./Replay.svelte";
import FxGallery from "./FxGallery.svelte";
import { fxForEvents, fxTtl, type BoardFx } from "./fx";
import { 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";
@@ -1076,7 +1077,7 @@
onclick={() => (setupColor = c)}
aria-label={`wizard color ${c + 1}${taken ? " (taken)" : ""}`}
>
<img src={`/tokens/wizard-${c}.png`} alt="" />
<img src={tokenArt(`wizard-${c}`, "players")} alt="" />
</button>
{/each}
</div>
@@ -1211,7 +1212,7 @@
{@const chosen = net.roomColors[p]}
<li>
{#if chosen !== undefined}
<img class="roster-standee" src={`/tokens/wizard-${chosen}.png`} alt="" />
<img class="roster-standee" src={tokenArt(`wizard-${chosen}`, "players")} alt="" />
{:else}
<span class="dot" style:background="#b3a687"></span>
{/if}
@@ -1231,7 +1232,7 @@
onclick={() => net.pickColor(c)}
aria-label={`wizard color ${c + 1}${takenBy && takenBy !== net.you ? ` (taken by ${takenBy})` : ""}`}
>
<img src={`/tokens/wizard-${c}.png`} alt="" />
<img src={tokenArt(`wizard-${c}`, "players")} alt="" />
</button>
{/each}
</div>
+7 -6
View File
@@ -2,6 +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 { FX_SPRITES } from "./fx-sprites";
const CELL = 48;
@@ -119,10 +120,10 @@
return 1;
}
function wizardArt(id: string): string {
return `/tokens/wizard-${sharedColorIndex(view, id) % 6}.png`;
return tokenArt(`wizard-${sharedColorIndex(view, id) % 6}`, "players");
}
function treasureArt(owner: string): string {
return `/tokens/treasure-${sharedColorIndex(view, owner) % 6}.png`;
return tokenArt(`treasure-${sharedColorIndex(view, owner) % 6}`, "objects");
}
function playerColor(id: string): string {
@@ -298,7 +299,7 @@
{@const sy = Number(key.split(",")[1])}
{#if USE_TOKEN_ART && TERRAIN_ART[content.kind]}
<image
href={`/tokens/${TERRAIN_ART[content.kind]}.png`}
href={tokenArt(TERRAIN_ART[content.kind]!, "terrain")}
x={sx * CELL + 3} y={sy * CELL + 3}
width={CELL - 6} height={CELL - 6}
preserveAspectRatio="xMidYMid slice"
@@ -328,7 +329,7 @@
{#each [w.a, w.b] as tok, i (i)}
{#if USE_TOKEN_ART}
<image
href="/tokens/dimensional-warp.png"
href={tokenArt("dimensional-warp", "terrain")}
x={tok.x * CELL + CELL * 0.04} y={tok.y * CELL + CELL * 0.04}
width={CELL * 0.44} height={CELL * 0.44}
preserveAspectRatio="xMidYMid slice" class="token-art"
@@ -356,7 +357,7 @@
{@const art = USE_TOKEN_ART ? objectArt(o.cardId) : null}
{#if art}
<image
href={`/tokens/${art}.png`}
href={tokenArt(art, "objects")}
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"
@@ -556,7 +557,7 @@
>
{#if USE_TOKEN_ART && CREATURE_ART[c.kind]}
<image
href={`/tokens/${CREATURE_ART[c.kind]}.png`}
href={tokenArt(CREATURE_ART[c.kind]!, "creatures")}
x={-CELL * 0.26} y={-CELL * 0.26}
width={CELL * 0.52} height={CELL * 0.52}
preserveAspectRatio="xMidYMid slice"
+17
View File
@@ -0,0 +1,17 @@
// The art workshop's side door (deliberately undocumented): query params
// like ?svgPlayers=true&svgTerrain=true swap token categories to the
// hand-drawn SVG set. public/tokens-svg symlinks research/tokens-svg, so
// under `npm run dev` an edit to the source art shows on refresh.
export type ArtCategory = "players" | "creatures" | "terrain" | "objects";
const q = new URLSearchParams(typeof location === "undefined" ? "" : location.search);
const SVG_ART: Record<ArtCategory, boolean> = {
players: q.get("svgPlayers") === "true",
creatures: q.get("svgCreatures") === "true",
terrain: q.get("svgTerrain") === "true",
objects: q.get("svgObjects") === "true",
};
export function tokenArt(file: string, cat: ArtCategory): string {
return SVG_ART[cat] ? `/tokens-svg/${file}.svg` : `/tokens/${file}.png`;
}