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:
co-authored by
Claude Fable 5
parent
ce556b5508
commit
ed7a58a698
@@ -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; }
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
import type { Component } from "svelte";
|
||||
import { FX_SPRITES } from "./fx-sprites";
|
||||
import type { BoardFx, FxShape } from "./fx";
|
||||
import FirewallEdge from "./FirewallEdge.svelte";
|
||||
import IllusionShimmer from "./IllusionShimmer.svelte";
|
||||
import SightTraceOverlay from "./SightTraceOverlay.svelte";
|
||||
|
||||
let tick = $state(0);
|
||||
$effect(() => {
|
||||
@@ -73,6 +76,63 @@
|
||||
</figure>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<h1 class="furniture-head">The standing fires</h1>
|
||||
<p class="sub">
|
||||
Board furniture that burns as long as its spell holds — these loop by
|
||||
nature, no beat needed. Same components the live board mounts.
|
||||
</p>
|
||||
<div class="grid">
|
||||
<figure>
|
||||
<svg viewBox="-4 -4 152 152">
|
||||
{#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}
|
||||
<FirewallEdge x={1} y={0} kind="H" />
|
||||
</svg>
|
||||
<figcaption>wall of fire (H)</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<svg viewBox="-4 -4 152 152">
|
||||
{#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}
|
||||
<FirewallEdge x={0} y={1} kind="V" />
|
||||
</svg>
|
||||
<figcaption>wall of fire (V)</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<svg viewBox="-4 -4 152 152">
|
||||
{#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}
|
||||
<rect x={93.5} y={44.5} width={7} height={55} class="demo-wall" />
|
||||
<IllusionShimmer x={1} y={1} kind="V" />
|
||||
</svg>
|
||||
<figcaption>illusion shimmer</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<svg viewBox="-4 -4 152 152">
|
||||
{#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}
|
||||
<SightTraceOverlay from={{ x: 0, y: 2 }} to={{ x: 2, y: 0 }} trace={{ kind: "direct" }} />
|
||||
</svg>
|
||||
<figcaption>sight trace (direct)</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<svg viewBox="-4 -4 152 152">
|
||||
{#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}
|
||||
<SightTraceOverlay from={{ x: 0, y: 2 }} to={{ x: 2, y: 1 }}
|
||||
trace={{ kind: "warp", mouthA: { cell: { x: 1, y: 0 }, side: "N" },
|
||||
mouthB: { cell: { x: 1, y: 2 }, side: "S" },
|
||||
entry: { x: 1.5, y: 0 }, exit: { x: 1.5, y: 3 } }} />
|
||||
</svg>
|
||||
<figcaption>sight trace (through a warp)</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -92,6 +152,15 @@
|
||||
margin: 0 0 0.2rem;
|
||||
}
|
||||
.sub { color: #8d8672; margin: 0 0 1.2rem; }
|
||||
.furniture-head {
|
||||
font-family: "Oswald", sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
font-size: 1.2rem;
|
||||
color: #e9e1cb;
|
||||
margin: 1.6rem 0 0.2rem;
|
||||
}
|
||||
.demo-wall { fill: #4d4438; }
|
||||
.sub code { color: #c9a72a; }
|
||||
.grid {
|
||||
display: grid;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
// An UNTESTED illusion wall: the wall renders solid underneath (it is a
|
||||
// wall to you until your eyes rule) — this is the tell-tale shimmer laid
|
||||
// over it, and the tap target that offers the belief test.
|
||||
let { x, y, kind, onclick }: {
|
||||
x: number; y: number; kind: "V" | "H";
|
||||
onclick?: () => void;
|
||||
} = $props();
|
||||
const CELL = 48;
|
||||
const x1 = $derived(kind === "V" ? (x + 1) * CELL : x * CELL + 2);
|
||||
const y1 = $derived(kind === "V" ? y * CELL + 2 : (y + 1) * CELL);
|
||||
const x2 = $derived(kind === "V" ? (x + 1) * CELL : (x + 1) * CELL - 2);
|
||||
const y2 = $derived(kind === "V" ? (y + 1) * CELL - 2 : (y + 1) * CELL);
|
||||
</script>
|
||||
|
||||
<line {x1} {y1} {x2} {y2} class="illusion-shimmer" />
|
||||
{#if onclick}
|
||||
<line {x1} {y1} {x2} {y2} class="illusion-hit"
|
||||
role="button" tabindex="-1" aria-label="a shimmering wall — test your eyes"
|
||||
onclick={(ev) => { ev.stopPropagation(); onclick?.(); }}
|
||||
onkeydown={() => {}} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.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; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
// The sight line an attack traveled — straight when direct, leg by leg
|
||||
// through both warp mouths (with pulsing rings) when the maze wrapped.
|
||||
import type { SightTrace } from "@wizwar/engine";
|
||||
let { from, to, trace }: {
|
||||
from: { x: number; y: number };
|
||||
to: { x: number; y: number };
|
||||
trace: SightTrace;
|
||||
} = $props();
|
||||
const CELL = 48;
|
||||
const A = $derived({ x: from.x * CELL + CELL / 2, y: from.y * CELL + CELL * 0.36 });
|
||||
const B = $derived({ x: to.x * CELL + CELL / 2, y: to.y * CELL + CELL * 0.36 });
|
||||
</script>
|
||||
|
||||
<g class="sight-layer" aria-hidden="true">
|
||||
{#if trace.kind === "direct"}
|
||||
<line x1={A.x} y1={A.y} x2={B.x} y2={B.y} class="sight-line" />
|
||||
{:else}
|
||||
{@const entry = { x: trace.entry.x * CELL, y: trace.entry.y * CELL }}
|
||||
{@const exit = { x: trace.exit.x * CELL, y: 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>
|
||||
|
||||
<style>
|
||||
.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: sight-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
@keyframes sight-march {
|
||||
to { stroke-dashoffset: -13; }
|
||||
}
|
||||
@keyframes sight-pulse {
|
||||
0%, 100% { opacity: 0.9; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sight-line, .sight-mouth { animation: none; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user