The audit Eric called for, delivered whole. Open doors are DOORWAYS now: stone jamb posts at both ends and a lintel hung across the top (paintable as textures/doorframe.png), the way through open to the eye. Battle damage shows — the engine's per-edge wallDamage wears painted cracks into any wall, heavier as harm mounts. The wall safe stands as a strongbox volume instead of a flat token. Hedges take root: an underbrush floor decal covers the whole square beneath them (Eric's report — a billboard at center depth can't reach the floor trapezoid's near edge), and the hedge itself widens past the cell. Rubble stretches across the fallen wall's full gap, corner piles landing at the posts, exactly as its art was painted. The first-person effect gaps close: victory now throws SIX fireworks climbing over the winner's home with gold washing the screen; sector rotations flash violet and heave the whole world; the Thumb of God lands with a burst and a shudder; walls conjured from nothing rise in stone dust; and every hazard pratfall plays — pit falls (with a dark drop and jolt for the faller's own eyes), ooze slips, tack yelps, thorn snaps, slime, dust. Dying in first person fades long and dark. And the whole tale travels: a finished game's full replay carries the share button too — turn -1 mints a link to every turn of the game, each through its wizard's own eyes, titled for the winner's triumph. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
238 lines
8.5 KiB
Svelte
238 lines
8.5 KiB
Svelte
<script lang="ts">
|
|
// The token workshop: every token in both arts, side by side — the
|
|
// photographed physical set and the hand-drawn SVG contingency. Open
|
|
// with /?tokens under `npm run dev`; edits to public/tokens-svg/*.svg
|
|
// show on the next refresh. Board size and close-up for each.
|
|
import TokenArt from "./TokenArt.svelte";
|
|
import EdgeGlyph, { type LockState } from "./EdgeGlyph.svelte";
|
|
import FirewallEdge from "./FirewallEdge.svelte";
|
|
import IllusionShimmer from "./IllusionShimmer.svelte";
|
|
import { FX_ART } from "./fpv/fx3d";
|
|
import { TERRAIN3D } from "./fpv/terrain3d";
|
|
|
|
const GROUPS: { title: string; files: string[] }[] = [
|
|
{ title: "Wizards", files: ["wizard-0", "wizard-1", "wizard-2", "wizard-3", "wizard-4", "wizard-5"] },
|
|
{ title: "Treasures", files: ["treasure-0", "treasure-1", "treasure-2", "treasure-3", "treasure-4", "treasure-5"] },
|
|
{
|
|
title: "Monsters",
|
|
files: ["troll", "skeleton", "wraith", "fire-imp", "democratic-monster", "shadow", "alter-ego"],
|
|
},
|
|
{
|
|
title: "Terrain",
|
|
files: [
|
|
"solid-stone", "thorn-bush", "rosebush", "killer-ooze", "slime", "dustcloud",
|
|
"tacks", "pit", "safe",
|
|
],
|
|
},
|
|
{
|
|
title: "Objects & markers",
|
|
files: [
|
|
"dagger", "rock", "magic-stone", "magic-wand", "master-key", "boobytrap",
|
|
"dimensional-warp",
|
|
],
|
|
},
|
|
];
|
|
|
|
// Edge treatments, not tokens — see EdgeGlyph. Shown exactly as the
|
|
// board mounts them.
|
|
const EDGE_PIECES: {
|
|
caption: string;
|
|
state?: "wall" | "door";
|
|
lock?: LockState | null;
|
|
special?: "firewall" | "shimmer" | "known-illusion";
|
|
}[] = [
|
|
{ caption: "wall", state: "wall" },
|
|
{ caption: "door (locked)", state: "door" },
|
|
{ caption: "door, picked open (ajar)", state: "door", lock: "ajar" },
|
|
{ caption: "door, held open", state: "door", lock: "held" },
|
|
{ caption: "door, lock removed", state: "door", lock: "removed" },
|
|
{ caption: "door, lock jammed", state: "door", lock: "jammed" },
|
|
{ caption: "wall of fire", special: "firewall" },
|
|
{ caption: "illusion wall (untested)", special: "shimmer" },
|
|
{ caption: "illusion wall (you know it's fake)", special: "known-illusion" },
|
|
];
|
|
</script>
|
|
|
|
<div class="gallery">
|
|
<h1>The Token Workshop</h1>
|
|
<p class="sub">
|
|
Every token, twice: the photographed physical set beside the hand-drawn
|
|
contingency. Edit <code>public/tokens-svg/</code> and refresh. Small is
|
|
board size; large is the close-up the peek modal shows.
|
|
<a href="/?fx">→ the flourish workshop</a>
|
|
</p>
|
|
{#each GROUPS as group (group.title)}
|
|
<h2>{group.title}</h2>
|
|
<div class="grid">
|
|
{#each group.files as f (f)}
|
|
<figure>
|
|
<div class="pair">
|
|
<div class="col">
|
|
<img class="big" src={`/tokens/${f}.png`} alt={`${f} (photo)`} />
|
|
<img class="small" src={`/tokens/${f}.png`} alt="" />
|
|
<span class="tag">photo</span>
|
|
</div>
|
|
<div class="col">
|
|
<svg viewBox="0 0 96 96" class="big">
|
|
<TokenArt href={`/tokens-svg/${f}.svg`} x={0} y={0} width={96} height={96} />
|
|
</svg>
|
|
<svg viewBox="0 0 28 28" class="small">
|
|
<TokenArt href={`/tokens-svg/${f}.svg`} x={0} y={0} width={28} height={28} />
|
|
</svg>
|
|
<span class="tag">drawn</span>
|
|
</div>
|
|
</div>
|
|
<figcaption>{f}</figcaption>
|
|
</figure>
|
|
{/each}
|
|
</div>
|
|
{/each}
|
|
|
|
<h2>The masonry — first-person wall textures</h2>
|
|
<p class="sub">
|
|
The materials the raycaster (<code>/?fpv</code>) wraps its walls in.
|
|
Each lives at <code>public/textures/<name>.png</code> — repaint the
|
|
file (any size) and refresh; the built-in procedural bake stands in
|
|
wherever a file is missing.
|
|
</p>
|
|
<div class="grid">
|
|
{#each ["wall", "rim", "stone", "door", "firewall", "warp", "doorframe", "cracks", "floor", "ceiling", "home", "pit", "slime", "tacks", "dimwarp", "underbrush"] as name (name)}
|
|
<figure>
|
|
<img class="masonry" src={`/textures/${name}.png`} alt={`${name} texture`} />
|
|
<figcaption>{name}</figcaption>
|
|
</figure>
|
|
{/each}
|
|
</div>
|
|
|
|
<h2>The conjurations — first-person spell sprites</h2>
|
|
<p class="sub">
|
|
The moments the raycaster throws through the air: projectiles in
|
|
flight, bursts on landing, shimmers and shields. Each lives at
|
|
<code>public/fx3d/<name>.png</code> — square, transparent ground,
|
|
drawn glowing mid-air at any size; a procedural glow stands in
|
|
wherever a file is missing.
|
|
</p>
|
|
<div class="grid">
|
|
{#each FX_ART as name (name)}
|
|
<figure>
|
|
<img class="masonry conjuration" src={`/fx3d/${name}.png`} alt={`${name} sprite`} />
|
|
<figcaption>{name}</figcaption>
|
|
</figure>
|
|
{/each}
|
|
</div>
|
|
|
|
<h2>The standing terrain — first-person volumes</h2>
|
|
<p class="sub">
|
|
Terrain that stands in the room: hedges filling a square to half
|
|
height, the killer ooze as a translucent cube, dust billowing, the
|
|
dimensional warp's pillar of light. Each lives at
|
|
<code>public/terrain3d/<name>.png</code> — transparent PNG, any
|
|
size; hedges render twice as wide as tall. Rehearse them in place with
|
|
<code>/?fpv&terrain=thornbush@3,7;jello@4,7</code>.
|
|
</p>
|
|
<div class="grid">
|
|
{#each TERRAIN3D as name (name)}
|
|
<figure>
|
|
<img class="masonry conjuration" src={`/terrain3d/${name}.png`} alt={`${name} sprite`} />
|
|
<figcaption>{name}</figcaption>
|
|
</figure>
|
|
{/each}
|
|
</div>
|
|
|
|
<h2>Walls & doors — as the maze draws them</h2>
|
|
<p class="sub">
|
|
Wall-segment effects have never been tokens: locks picked, jammed, and
|
|
removed, held doors, standing fires, and illusions are edge treatments,
|
|
shown here with the very components the board mounts.
|
|
</p>
|
|
<div class="grid">
|
|
{#each EDGE_PIECES as piece (piece.caption)}
|
|
<figure>
|
|
<svg viewBox="20 20 104 104" class="edge-demo">
|
|
{#each [0, 1, 2] as cy (cy)}{#each [0, 1, 2] as cx (cx)}
|
|
<rect x={cx * 48} y={cy * 48} width="48" height="48" class="cell" />
|
|
{/each}{/each}
|
|
{#if piece.special === "firewall"}
|
|
<FirewallEdge x={1} y={0} kind="H" />
|
|
{:else if piece.special === "shimmer"}
|
|
<EdgeGlyph x={1} y={0} kind="H" state="wall" />
|
|
<IllusionShimmer x={1} y={0} kind="H" />
|
|
{:else if piece.special === "known-illusion"}
|
|
<line x1={48} y1={48} x2={96} y2={48} class="known-illusion" />
|
|
{:else}
|
|
<EdgeGlyph x={1} y={0} kind="H" state={piece.state!} lock={piece.lock ?? null} />
|
|
{/if}
|
|
</svg>
|
|
<figcaption>{piece.caption}</figcaption>
|
|
</figure>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.gallery {
|
|
min-height: 100vh;
|
|
background: #171a20;
|
|
color: #d8d2c0;
|
|
font-family: "Archivo Narrow", system-ui, sans-serif;
|
|
padding: 1.5rem;
|
|
}
|
|
h1, h2 {
|
|
font-family: "Oswald", sans-serif;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.14em;
|
|
color: #e9e1cb;
|
|
margin: 0 0 0.2rem;
|
|
}
|
|
h1 { font-size: 1.2rem; }
|
|
h2 { font-size: 0.95rem; margin-top: 1.4rem; }
|
|
.sub { color: #8d8672; margin: 0 0 1rem; }
|
|
.sub code { color: #c9a72a; }
|
|
.sub a { color: #c9a72a; }
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
|
gap: 0.9rem;
|
|
}
|
|
figure {
|
|
margin: 0;
|
|
background: #efe8d4;
|
|
border-radius: 4px;
|
|
padding: 0.5rem;
|
|
}
|
|
.pair { display: flex; gap: 0.6rem; justify-content: center; }
|
|
.col { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; }
|
|
.big { width: 72px; height: 72px; display: block; }
|
|
.small { width: 28px; height: 28px; display: block; }
|
|
img.big, img.small { object-fit: cover; border-radius: 3px; }
|
|
.tag {
|
|
font-family: "Courier Prime", monospace;
|
|
font-size: 0.6rem;
|
|
color: #8a7a5e;
|
|
}
|
|
.edge-demo { width: 100%; display: block; }
|
|
.cell {
|
|
fill: #efe8d4;
|
|
stroke: rgba(95, 74, 51, 0.25);
|
|
stroke-width: 1;
|
|
}
|
|
/* Mirrors Board's known-illusion ghost: dashed, violet, see-through. */
|
|
.known-illusion { stroke: #7a6f9a; stroke-width: 4; stroke-dasharray: 6 5; opacity: 0.7; }
|
|
figcaption {
|
|
font-family: "Courier Prime", monospace;
|
|
font-size: 0.72rem;
|
|
color: #43331f;
|
|
text-align: center;
|
|
padding-top: 0.35rem;
|
|
}
|
|
.masonry {
|
|
width: 128px;
|
|
height: 128px;
|
|
image-rendering: pixelated;
|
|
border: 1px solid #3a3428;
|
|
background: #101318;
|
|
}
|
|
/* Sprites fly against darkness; show them on it. */
|
|
.conjuration { background: #14101c; }
|
|
</style>
|