Three blind reviews over the first-person arc, every finding verified against the source. History-narrating comments made timeless or cut; the stacked BUDDY comment collapsed to one voice. Dead code out: the orphaned FACE copy, the DIR_ANGLE duplicate, the dead loop-counter poke, the impossible-state sentinel. The never-produced "warp" hit kind resolved the right way — warp mouths now hang a translucent veil of the painted warp texture, so art that never rendered finally does. Types tightened (SlabStrike named once, ShareData rides CatchUpStep, botTier loses its casts), the gallery reads MATERIALS instead of a hand-copied list, the chronicle resets through one helper, a superseded share mint rejects instead of stranding, and the conjured safe gains the growth key its siblings had. Tests lose a triple assignment, a tautology, and two self-swallowing regex alternatives. The sprite spec — which still told the artist to paint for additive compositing the renderer no longer uses — now describes the renderer that exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
239 lines
8.4 KiB
Svelte
239 lines
8.4 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 { MATERIALS } from "./fpv/textures";
|
|
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 MATERIALS 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>
|