Engine: pit-rim exits live in one helper (pitRimExits) shared by the resolver and the automaton; chargeReach names the six-stride cap; dust sight is inDust + dustAtEnd; PushPending is exported once; the force-field counter builds its events in one list; drawUnderSlowDeath says what it draws under. Test groups are named for the rule they pin, not the revision that introduced it. Client: one smoothstep; one edgeScar per battle-scarred edge; one board-zone rule (which also seats the caption strip on phones); the compass names live in SIDE_NAMES; net.flash() is the one toast; MediaQuery replaces two hand-rolled matchMedia listeners; sprites carry their sizes as plain numbers and their CSS in one rule each; canvas helpers are formatted like the rest of the tree. Comments that told how a cel or a fallback used to look now say what it draws. The actions-over notice no longer blames a pickup when slime ended the turn. Server and ops: dataRoot() is the one data directory; headlineOf builds its deeds without a cast; the rollup and skills read the same way the code behaves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
97 lines
3.6 KiB
Svelte
97 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
// A standing WALL OF FIRE: board furniture, not a flourish — it burns for
|
|
// as long as the spell holds, so the animation is pure CSS on a handful
|
|
// of flat cel shapes, with a short held-pose flicker.
|
|
import { holdToPeek } from "./peek";
|
|
let { x, y, kind, onpeek }: {
|
|
x: number; y: number; kind: "V" | "H";
|
|
/** Press-and-hold (touch has no hover): report what this fire is. */
|
|
onpeek?: (tip: string) => void;
|
|
} = $props();
|
|
const TIP = "a wall of fire — passing through burns for 4";
|
|
const { press, release } = holdToPeek(() => onpeek?.(TIP));
|
|
const CELL = 48;
|
|
const WALL = 7;
|
|
const cx = $derived(kind === "V" ? (x + 1) * CELL : x * CELL + CELL / 2);
|
|
const cy = $derived(kind === "V" ? y * CELL + CELL / 2 : (y + 1) * CELL);
|
|
// Tongues sit along the edge's axis; every flame rises screen-up.
|
|
const TONGUES = [
|
|
{ t: -0.38, h: 11, d: 0.0 },
|
|
{ t: -0.19, h: 15, d: 0.45 },
|
|
{ t: 0.0, h: 18, d: 0.15 },
|
|
{ t: 0.21, h: 13, d: 0.6 },
|
|
{ t: 0.4, h: 16, d: 0.3 },
|
|
];
|
|
const EMBERS = [
|
|
{ t: -0.28, d: 0.0 },
|
|
{ t: 0.08, d: 0.9 },
|
|
{ t: 0.33, d: 1.7 },
|
|
];
|
|
const px = (t: number) => (kind === "V" ? 0 : t * CELL);
|
|
const py = (t: number) => (kind === "V" ? t * CELL : 0);
|
|
</script>
|
|
|
|
<g transform={`translate(${cx} ${cy})`} class="fw">
|
|
<title>{TIP}</title>
|
|
<!-- the burning bar itself, seated on the wall line -->
|
|
{#if kind === "V"}
|
|
<rect x={-WALL / 2} y={-CELL / 2 - 2} width={WALL} height={CELL + 4} rx="3"
|
|
fill="#bd3b1b" class="fw-bar"
|
|
role="img" onpointerdown={press} onpointerup={release} onpointerleave={release} />
|
|
{:else}
|
|
<rect x={-CELL / 2 - 2} y={-WALL / 2} width={CELL + 4} height={WALL} rx="3"
|
|
fill="#bd3b1b" class="fw-bar"
|
|
role="img" onpointerdown={press} onpointerup={release} onpointerleave={release} />
|
|
{/if}
|
|
<!-- tongues of flame, each on its own beat; the bar beneath is the hit target -->
|
|
{#each TONGUES as f (f.t)}
|
|
<g transform={`translate(${px(f.t)} ${py(f.t)})`} class="fw-art">
|
|
<g class="fw-tongue" style={`animation-delay: ${-f.d}s`}>
|
|
<path d={`M -3.5 1 Q -6 ${-f.h*.35} -1.7 ${-f.h*.61} Q 1 ${-f.h*.81} -.5 ${-f.h} Q 5 ${-f.h*.82} 2.5 ${-f.h*.47} Q 6 ${-f.h*.6} 5 ${-f.h*.75} Q 9 ${-f.h*.27} 3.4 1 Z`}
|
|
fill="#ef681e" stroke="#922e1a" stroke-width="0.65" stroke-linejoin="round" />
|
|
<path d={`M -1.8 1 Q -3 ${-f.h*.25} 1 ${-f.h*.58} Q .1 ${-f.h*.2} 3 ${-f.h*.34} Q 4 ${-f.h*.12} 1.8 1 Z`} fill="#ffd34e" />
|
|
<path d={`M -.5 1 Q -1 ${-f.h*.12} 1.2 ${-f.h*.28} L 1.4 1 Z`} fill="#fff09a" />
|
|
</g>
|
|
</g>
|
|
{/each}
|
|
{#each EMBERS as em (em.t)}
|
|
<path d={`M ${px(em.t)} ${py(em.t)-9} q -3 3 -.5 5 q 3 -1 .5 -5 Z`} class="fw-ember"
|
|
style={`animation-delay: ${-em.d}s`} />
|
|
{/each}
|
|
</g>
|
|
|
|
<style>
|
|
.fw-art, .fw-ember { pointer-events: none; }
|
|
.fw-bar {
|
|
stroke: #7c1a14;
|
|
stroke-width: 1;
|
|
animation: fw-coals 1.3s steps(2, end) infinite alternate;
|
|
}
|
|
.fw-tongue {
|
|
transform-origin: 0px 1px;
|
|
animation: fw-lick 0.9s steps(3, end) infinite alternate;
|
|
}
|
|
.fw-ember {
|
|
fill: #ffd166;
|
|
opacity: 0;
|
|
animation: fw-rise 2.6s ease-out infinite;
|
|
}
|
|
@keyframes fw-coals {
|
|
from { fill: #bd3b1b; }
|
|
to { fill: #d64b1c; }
|
|
}
|
|
@keyframes fw-lick {
|
|
from { transform: scale(0.95, 0.82) rotate(-4deg); opacity: 1; }
|
|
to { transform: scale(1.04, 1.08) rotate(3deg); opacity: 1; }
|
|
}
|
|
@keyframes fw-rise {
|
|
0% { opacity: 0; transform: translateY(0); }
|
|
15% { opacity: 0.9; }
|
|
100% { opacity: 0; transform: translateY(-16px); }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.fw-bar, .fw-tongue, .fw-ember { animation: none; }
|
|
.fw-ember { opacity: 0.6; }
|
|
}
|
|
</style>
|