The workshop gains 'The standing fires': board furniture on display

FirewallEdge (both orientations), the illusion shimmer, and the sight
trace (direct and through-a-warp) join the /?fx gallery — and to keep
the display honest, the shimmer and sight trace were extracted from
Board.svelte's inline markup into IllusionShimmer.svelte and
SightTraceOverlay.svelte, so the workshop mounts the very components
the live board does. No copies to drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-18 09:13:31 -04:00
co-authored by Claude Fable 5
parent ce556b5508
commit ed7a58a698
4 changed files with 171 additions and 60 deletions
+6 -60
View File
@@ -6,6 +6,8 @@
import { colorIndexOf as sharedColorIndex, wizardColor } from "./colors";
import { CREATURE_ART, objectArt, TERRAIN_ART, tokenArt } from "./art";
import FirewallEdge from "./FirewallEdge.svelte";
import IllusionShimmer from "./IllusionShimmer.svelte";
import SightTraceOverlay from "./SightTraceOverlay.svelte";
import TokenArt from "./TokenArt.svelte";
import { FX_SPRITES } from "./fx-sprites";
@@ -437,18 +439,11 @@
<!-- untested illusions: the wall stands, but it shimmers — click to doubt it -->
{#each Object.entries(view.illusionEdges) as [key, verdict] (key)}
{#if verdict === "untested"}
{@const kind = key.split(":")[0]}
{@const kind = key.split(":")[0] === "V" ? "V" as const : "H" as const}
{@const ix = Number(key.split(":")[1]?.split(",")[0])}
{@const iy = Number(key.split(":")[1]?.split(",")[1])}
{@const x1 = kind === "V" ? (ix + 1) * CELL : ix * CELL + 2}
{@const y1 = kind === "V" ? iy * CELL + 2 : (iy + 1) * CELL}
{@const x2 = kind === "V" ? (ix + 1) * CELL : (ix + 1) * CELL - 2}
{@const y2 = kind === "V" ? (iy + 1) * CELL - 2 : (iy + 1) * CELL}
<line {x1} {y1} {x2} {y2} class="illusion-shimmer" />
<line {x1} {y1} {x2} {y2} class="illusion-hit"
role="button" tabindex="-1" aria-label="a shimmering wall — test your eyes"
onclick={(ev) => { ev.stopPropagation(); onIllusionClick?.({ x: ix, y: iy }, kind === "V" ? "E" : "S"); }}
onkeydown={() => {}} />
<IllusionShimmer x={ix} y={iy} {kind}
onclick={() => onIllusionClick?.({ x: ix, y: iy }, kind === "V" ? "E" : "S")} />
{/if}
{/each}
@@ -645,20 +640,7 @@
{/if}
<!-- the sight line an attack traveled, leg by leg through any warp mouth -->
{#if sightTrace}
{@const A = { x: sightTrace.from.x * CELL + CELL / 2, y: sightTrace.from.y * CELL + CELL * 0.36 }}
{@const B = { x: sightTrace.to.x * CELL + CELL / 2, y: sightTrace.to.y * CELL + CELL * 0.36 }}
<g class="sight-layer" aria-hidden="true">
{#if sightTrace.trace.kind === "direct"}
<line x1={A.x} y1={A.y} x2={B.x} y2={B.y} class="sight-line" />
{:else}
{@const entry = { x: sightTrace.trace.entry.x * CELL, y: sightTrace.trace.entry.y * CELL }}
{@const exit = { x: sightTrace.trace.exit.x * CELL, y: sightTrace.trace.exit.y * CELL }}
<line x1={A.x} y1={A.y} x2={entry.x} y2={entry.y} class="sight-line" />
<line x1={exit.x} y1={exit.y} x2={B.x} y2={B.y} class="sight-line" />
<circle cx={entry.x} cy={entry.y} r={7} class="sight-mouth" />
<circle cx={exit.x} cy={exit.y} r={7} class="sight-mouth" />
{/if}
</g>
<SightTraceOverlay from={sightTrace.from} to={sightTrace.to} trace={sightTrace.trace} />
{/if}
<!-- spell flourishes: one sprite component per effect (src/fx-sprites/) -->
<g class="fx-layer" aria-hidden="true">
@@ -737,23 +719,6 @@
.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; }
.illusion-shimmer {
stroke: #cfc3f0;
stroke-width: 3;
stroke-dasharray: 4 5;
stroke-linecap: round;
pointer-events: none;
animation: shimmer-drift 1.4s linear infinite;
}
.illusion-hit { stroke: transparent; stroke-width: 14; cursor: pointer; }
@keyframes shimmer-drift {
0% { stroke-dashoffset: 0; opacity: 0.9; }
50% { opacity: 0.4; }
100% { stroke-dashoffset: -18; opacity: 0.9; }
}
@media (prefers-reduced-motion: reduce) {
.illusion-shimmer { animation: none; opacity: 0.7; }
}
.creature { cursor: pointer; }
.creature-body { fill: #3b3428; stroke-width: 2.5; }
.creature-body.selected { fill: #6b5a34; }
@@ -823,24 +788,6 @@
.ghost-slot:hover { fill: rgba(122, 162, 122, 0.22); }
.fx-layer { pointer-events: none; }
.sight-layer { pointer-events: none; }
.sight-line {
stroke: #c9a72a;
stroke-width: 2.5;
stroke-linecap: round;
stroke-dasharray: 7 6;
opacity: 0.85;
animation: sight-march 0.8s linear infinite;
}
.sight-mouth {
fill: none;
stroke: #c9a72a;
stroke-width: 2;
animation: warp-pulse 1.6s ease-in-out infinite;
}
@keyframes sight-march {
to { stroke-dashoffset: -13; }
}
.mover { transition: transform 260ms cubic-bezier(0.25, 0.8, 0.35, 1); }
.mover.snap { transition: none; }
@@ -867,7 +814,6 @@
}
@media (prefers-reduced-motion: reduce) {
.marked-cell, .marked-sector, .ghost-slot, .warp-dest { animation: none; }
.sight-line, .sight-mouth { animation: none; }
.fx-layer { display: none; }
:global(.token-art.ghosted) { animation: none; }
.mover { transition: none; }