Flourishes third wave: the grind, the grave, and the pratfalls

The showstopper: sectors visibly move — a dashed parchment ghost
rotates through its quarter-turn or slides the whole way to its new
ground (corrected for rev-11 renormalization, since the event records
pre-shift origins and the view holds the truth). Death sends a small
soul drifting up from the token; victory bursts fireworks over the
winner's home, three volleys; CHAOS spins a triple spiral over the
whole maze and stands alone in its batch.

And the slapstick tier: knockbacks, shoves, drags, and pit-jumps
leave a motion streak; falling in a pit swallows dark with dust;
ooze gets a wobbling pratfall ellipse; tacks sting in three hopping
asterisks; thornbush snaps green; slime sinks its ring; a successful
pit climb puffs dust.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-17 01:12:22 -04:00
co-authored by Claude Fable 5
parent 6da350fd46
commit 68b0da6301
2 changed files with 278 additions and 6 deletions
+195
View File
@@ -666,6 +666,74 @@
{@const c = center(fx.at)}
<circle cx={c.x} cy={c.y} r="8" class="fx-warpglow" />
<circle cx={c.x} cy={c.y} r="8" class="fx-warpglow late" />
{:else if fx.kind === "streak"}
{@const a = center(fx.from)}
{@const b = center(fx.to)}
<line x1={a.x} y1={a.y} x2={b.x} y2={b.y} class="fx-streak" />
{:else if fx.kind === "soul"}
{@const c = center(fx.at)}
<g class="fx-soul">
<circle cx={c.x} cy={c.y - 4} r="8" />
<circle cx={c.x - 5} cy={c.y + 3} r="4" />
<circle cx={c.x + 5} cy={c.y + 3} r="4" />
</g>
{:else if fx.kind === "fireworks"}
{@const c = center(fx.at)}
<g class="fx-fireworks" style={`transform-origin: ${c.x}px ${c.y}px`}>
{#each [0, 45, 90, 135, 180, 225, 270, 315] as deg (deg)}
<circle
cx={c.x + 18 * Math.cos((deg * Math.PI) / 180)}
cy={c.y + 18 * Math.sin((deg * Math.PI) / 180)}
r="3" fill={["#e74c3c", "#f1c40f", "#3b8dd6", "#7ac47e"][(deg / 45) % 4]}
/>
{/each}
</g>
{:else if fx.kind === "chaos-swirl"}
{@const c = center(fx.at)}
<g class="fx-chaos" style={`transform-origin: ${c.x}px ${c.y}px`}>
<circle cx={c.x} cy={c.y} r="30" />
<circle cx={c.x} cy={c.y} r="55" class="mid" />
<circle cx={c.x} cy={c.y} r="80" class="outer" />
</g>
{:else if fx.kind === "pit-fall"}
{@const c = center(fx.at)}
<circle cx={c.x} cy={c.y} r="10" class="fx-pitfall" />
<circle cx={c.x - 9} cy={c.y - 4} r="3" class="fx-dust" />
<circle cx={c.x + 9} cy={c.y - 4} r="3" class="fx-dust late" />
{:else if fx.kind === "ooze-slip"}
{@const c = center(fx.at)}
<ellipse cx={c.x} cy={c.y + 8} rx="13" ry="5" class="fx-ooze" />
{:else if fx.kind === "tacks-ow"}
{@const c = center(fx.at)}
<g class="fx-tacks">
<text x={c.x - 10} y={c.y - 4}></text>
<text x={c.x + 4} y={c.y - 10} class="late"></text>
<text x={c.x - 2} y={c.y + 6} class="later"></text>
</g>
{:else if fx.kind === "thorn-snap"}
{@const c = center(fx.at)}
<path class="fx-thorn"
d={`M ${c.x - 12} ${c.y} l 6 -4 2 -8 4 6 8 -4 -3 8 7 5 -9 1 -2 9 -5 -7 -8 2 z`} />
{:else if fx.kind === "slime-stuck"}
{@const c = center(fx.at)}
<circle cx={c.x} cy={c.y} r="12" class="fx-slime" />
{:else if fx.kind === "dust-puff"}
{@const c = center(fx.at)}
<circle cx={c.x - 6} cy={c.y} r="4" class="fx-dust" />
<circle cx={c.x + 5} cy={c.y - 3} r="3" class="fx-dust late" />
<circle cx={c.x} cy={c.y + 4} r="3.5" class="fx-dust later" />
{:else if fx.kind === "sector-spin"}
{@const ox = fx.origin.x * CELL}
{@const oy = fx.origin.y * CELL}
<rect x={ox + 2} y={oy + 2} width={SECTOR * CELL - 4} height={SECTOR * CELL - 4}
class={`fx-sector-spin ${fx.clockwise ? "cw" : "ccw"}`}
style={`transform-origin: ${ox + (SECTOR * CELL) / 2}px ${oy + (SECTOR * CELL) / 2}px`} />
{:else if fx.kind === "sector-slide"}
{@const fxp = fx.from.x * CELL}
{@const fyp = fx.from.y * CELL}
<rect x={fxp + 2} y={fyp + 2} width={SECTOR * CELL - 4} height={SECTOR * CELL - 4}
class="fx-sector-slide"
style={`--dx: ${(fx.to.x - fx.from.x) * CELL}px; --dy: ${(fx.to.y - fx.from.y) * CELL}px`} />
{:else if fx.kind === "edge-dust"}
{@const m = edgeMid(fx.cell, fx.side)}
<circle cx={m.x - 6} cy={m.y} r="4" class="fx-dust" />
@@ -931,6 +999,133 @@
animation: fx-ring 0.55s ease-out forwards;
}
.fx-warpglow.late { stroke: #7ac47e; animation-delay: 0.15s; opacity: 0; }
.fx-streak {
stroke: rgba(233, 225, 203, 0.85);
stroke-width: 5;
stroke-linecap: round;
stroke-dasharray: 14 9;
animation: fx-streak-fade 0.55s ease-out forwards;
}
@keyframes fx-streak-fade {
0% { opacity: 0.9; stroke-dashoffset: 46; }
100% { opacity: 0; stroke-dashoffset: 0; }
}
.fx-soul circle {
fill: rgba(233, 228, 245, 0.8);
stroke: rgba(160, 150, 200, 0.6);
stroke-width: 1;
}
.fx-soul { animation: fx-ascend 1.3s ease-out forwards; }
@keyframes fx-ascend {
0% { opacity: 0; transform: translateY(0); }
25% { opacity: 0.9; }
100% { opacity: 0; transform: translateY(-34px); }
}
.fx-fireworks { animation: fx-fireworks-boom 1.1s ease-out forwards; }
@keyframes fx-fireworks-boom {
0% { opacity: 0; transform: scale(0.1); }
25% { opacity: 1; }
100% { opacity: 0; transform: scale(2.4) rotate(30deg); }
}
.fx-chaos circle {
fill: none;
stroke: #b48ae0;
stroke-width: 4;
stroke-dasharray: 30 22;
}
.fx-chaos .mid { stroke: #8a5fc0; stroke-dasharray: 44 30; }
.fx-chaos .outer { stroke: #e2c8ff; stroke-dasharray: 60 40; }
.fx-chaos { animation: fx-chaos-spin 1.4s ease-in-out forwards; }
@keyframes fx-chaos-spin {
0% { opacity: 0; transform: rotate(0deg) scale(0.6); }
20% { opacity: 1; }
100% { opacity: 0; transform: rotate(200deg) scale(1.5); }
}
.fx-pitfall {
transform-box: fill-box;
transform-origin: center;
fill: rgba(30, 24, 16, 0.8);
animation: fx-swallow 0.7s ease-in forwards;
}
.fx-ooze {
transform-box: fill-box;
transform-origin: center;
fill: rgba(110, 160, 60, 0.55);
animation: fx-ooze-wobble 0.7s ease-out forwards;
}
@keyframes fx-ooze-wobble {
0% { opacity: 0.9; transform: scaleX(0.6); }
35% { transform: scaleX(1.25); }
65% { transform: scaleX(0.9); }
100% { opacity: 0; transform: scaleX(1.1); }
}
.fx-tacks text {
font-size: 13px;
fill: #b3372b;
animation: fx-hop 0.6s ease-out forwards;
}
.fx-tacks .late { animation-delay: 0.1s; opacity: 0; }
.fx-tacks .later { animation-delay: 0.2s; opacity: 0; }
@keyframes fx-hop {
0% { opacity: 0; transform: translateY(2px); }
30% { opacity: 1; transform: translateY(-5px); }
60% { transform: translateY(-1px); }
100% { opacity: 0; transform: translateY(-8px); }
}
.fx-thorn {
fill: rgba(70, 120, 50, 0.7);
stroke: #2f5423;
stroke-width: 1.5;
transform-box: fill-box;
transform-origin: center;
animation: fx-pow-hit 0.55s ease-out forwards;
}
.fx-slime {
transform-box: fill-box;
transform-origin: center;
fill: rgba(140, 200, 60, 0.5);
stroke: #7a9e2e;
stroke-width: 3;
animation: fx-slime-sink 0.9s ease-out forwards;
}
@keyframes fx-slime-sink {
0% { opacity: 0.9; transform: scale(1.3); }
100% { opacity: 0; transform: scale(0.7); }
}
.fx-sector-spin {
fill: rgba(233, 225, 203, 0.25);
stroke: #d3852b;
stroke-width: 3;
stroke-dasharray: 12 7;
animation: fx-grind-cw 1.2s ease-in-out forwards;
}
.fx-sector-spin.ccw { animation-name: fx-grind-ccw; }
@keyframes fx-grind-cw {
0% { opacity: 0; transform: rotate(-90deg); }
15% { opacity: 1; }
85% { opacity: 1; transform: rotate(0deg); }
100% { opacity: 0; }
}
@keyframes fx-grind-ccw {
0% { opacity: 0; transform: rotate(90deg); }
15% { opacity: 1; }
85% { opacity: 1; transform: rotate(0deg); }
100% { opacity: 0; }
}
.fx-sector-slide {
fill: rgba(233, 225, 203, 0.25);
stroke: #d3852b;
stroke-width: 3;
stroke-dasharray: 12 7;
animation: fx-slide-home 1.2s ease-in-out forwards;
}
@keyframes fx-slide-home {
0% { opacity: 0; transform: translate(0, 0); }
15% { opacity: 1; }
85% { opacity: 1; transform: translate(var(--dx), var(--dy)); }
100% { opacity: 0; transform: translate(var(--dx), var(--dy)); }
}
/* --- persistent ambiance: ongoing spells wear their look ------------ */
.token-art.ghosted { opacity: 0.4; animation: ghost-breathe 2.6s ease-in-out infinite; }
@keyframes ghost-breathe { 50% { opacity: 0.22; } }