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
+83 -6
View File
@@ -8,9 +8,12 @@ type Cell = { x: number; y: number };
type Side = "N" | "E" | "S" | "W";
type FxShape =
| { kind: "fireball" | "bolt" | "waterbolt"; from: Cell; to: Cell }
| { kind: "fireball" | "bolt" | "waterbolt" | "streak"; from: Cell; to: Cell }
| { kind: "burst" | "splash" | "shimmer" | "shield" | "sparkle" | "whiff" | "hit"
| "pow" | "claw" | "absorb" | "warp-glow"; at: Cell }
| "pow" | "claw" | "absorb" | "warp-glow" | "soul" | "fireworks" | "chaos-swirl"
| "pit-fall" | "ooze-slip" | "tacks-ow" | "thorn-snap" | "slime-stuck" | "dust-puff"; at: Cell }
| { kind: "sector-spin"; origin: Cell; clockwise: boolean }
| { kind: "sector-slide"; from: Cell; to: Cell }
| { kind: "edge-dust"; cell: Cell; side: Side };
export type BoardFx = FxShape & { id: number };
@@ -20,7 +23,9 @@ let nextId = 1;
export function fxTtl(kind: BoardFx["kind"]): number {
switch (kind) {
case "fireball": case "waterbolt": return 700;
case "bolt": return 600;
case "bolt": case "streak": return 600;
case "soul": case "fireworks": case "chaos-swirl":
case "sector-spin": case "sector-slide": return 1500;
default: return 900;
}
}
@@ -52,6 +57,11 @@ export function fxForEvents(
for (const e of events) {
switch (e.type) {
case "spellCast": {
if (e.cardId === "chaos") {
push({ kind: "chaos-swirl", at: { x: (view.board.width - 1) / 2, y: (view.board.height - 1) / 2 } });
beat++;
break;
}
const to = e.targetCell ?? posOf(e.target);
const projectile = PROJECTILES[e.cardId];
if (projectile && to) {
@@ -136,10 +146,77 @@ export function fxForEvents(
push({ kind: "edge-dust", cell: e.edge.cell, side: e.edge.side });
beat++;
break;
case "sectorRotated":
case "sectorRelocated":
// The whole maze moved; any effect would point at stale ground.
case "died": {
const at = posOf(e.player);
if (at) { push({ kind: "soul", at }, 250); beat++; }
break;
}
case "gameWon": {
const home = view.players.find((p) => p.id === e.player)?.home;
if (home) {
push({ kind: "fireworks", at: home });
push({ kind: "fireworks", at: home }, 350);
push({ kind: "fireworks", at: home }, 700);
beat++;
}
break;
}
case "knockedBack":
case "shoved":
push({ kind: "streak", from: e.from, to: e.to });
beat++;
break;
case "objectDragged":
push({ kind: "streak", from: e.from, to: e.to });
beat++;
break;
case "jumpedPit":
push({ kind: "streak", from: e.from, to: e.to });
beat++;
break;
case "fellInPit":
push({ kind: "pit-fall", at: e.at });
beat++;
break;
case "climbedFromPit": {
if (e.success) {
const at = posOf(e.player);
if (at) { push({ kind: "dust-puff", at }); beat++; }
}
break;
}
case "slippedInOoze":
push({ kind: "ooze-slip", at: e.at });
beat++;
break;
case "steppedOnTacks":
push({ kind: "tacks-ow", at: e.at });
beat++;
break;
case "enteredThornbush":
push({ kind: "thorn-snap", at: e.at });
beat++;
break;
case "stuckInSlime":
push({ kind: "slime-stuck", at: e.at });
beat++;
break;
case "sectorRotated": {
const origin = view.board.placements[e.sectorIndex]?.origin;
if (origin) push({ kind: "sector-spin", origin: { ...origin }, clockwise: e.clockwise });
// Everything after the grind points at pre-move ground; stop here.
return out;
}
case "sectorRelocated": {
// The event records pre-normalization origins; the view holds the
// truth. Shift the recorded start by the same correction.
const trueTo = view.board.placements[e.sectorIndex]?.origin;
if (trueTo) {
const dx = trueTo.x - e.to.x, dy = trueTo.y - e.to.y;
push({ kind: "sector-slide", from: { x: e.from.x + dx, y: e.from.y + dy }, to: { ...trueTo } });
}
return out;
}
default:
break;
}