Flourishes: the spells become visible (branch only — not for the droplet yet)
A cosmetic effects layer on the board svg, fed by the same event stream that writes the log. Fireballs streak and burst; waterbolts arc and splash; lightning jags and flickers; teleports shimmer at both ends; walls rise and fall in dust; stopped attacks flash a golden shield; hits ring red; misses whiff; other attack spells sparkle at the caster. Successive visuals from one command stagger by a beat, everything self-expires, reduced-motion hides the layer whole, and nothing here touches game state — the effects are mapped from events in fx.ts and drawn in Board.svelte, online and hotseat alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b01e27c644
commit
bca34ae9e4
@@ -0,0 +1,107 @@
|
||||
// Whimsical flourishes: game events become short-lived board effects.
|
||||
// Purely cosmetic — nothing here touches game state, and reduced-motion
|
||||
// hides the whole layer.
|
||||
|
||||
import type { GameEvent, GameView } from "@wizwar/engine";
|
||||
|
||||
type Cell = { x: number; y: number };
|
||||
type Side = "N" | "E" | "S" | "W";
|
||||
|
||||
type FxShape =
|
||||
| { kind: "fireball" | "bolt" | "waterbolt"; from: Cell; to: Cell }
|
||||
| { kind: "burst" | "splash" | "shimmer" | "shield" | "sparkle" | "whiff" | "hit"; at: Cell }
|
||||
| { kind: "edge-dust"; cell: Cell; side: Side };
|
||||
export type BoardFx = FxShape & { id: number };
|
||||
|
||||
let nextId = 1;
|
||||
|
||||
/** How long each kind stays mounted (animation length + a little grace). */
|
||||
export function fxTtl(kind: BoardFx["kind"]): number {
|
||||
switch (kind) {
|
||||
case "fireball": case "waterbolt": return 700;
|
||||
case "bolt": return 600;
|
||||
default: return 900;
|
||||
}
|
||||
}
|
||||
|
||||
const PROJECTILES: Record<string, "fireball" | "bolt" | "waterbolt"> = {
|
||||
fireball: "fireball",
|
||||
"sudden-death": "fireball",
|
||||
"blaster-wand": "fireball",
|
||||
"lightning-blast": "bolt",
|
||||
"power-drain": "bolt",
|
||||
waterbolt: "waterbolt",
|
||||
};
|
||||
|
||||
/** Map one command's events to effects, each with a start delay in ms. */
|
||||
export function fxForEvents(
|
||||
events: GameEvent[], view: GameView,
|
||||
): { fx: BoardFx; delay: number }[] {
|
||||
const out: { fx: BoardFx; delay: number }[] = [];
|
||||
const posOf = (playerId: string | null): Cell | null => {
|
||||
if (!playerId) return null;
|
||||
const p = view.players.find((p) => p.id === playerId);
|
||||
return p ? { ...p.position } : null;
|
||||
};
|
||||
let beat = 0; // successive visuals from one command stagger slightly
|
||||
const push = (fx: FxShape, extraDelay = 0) => {
|
||||
out.push({ fx: { ...fx, id: nextId++ }, delay: beat * 220 + extraDelay });
|
||||
};
|
||||
|
||||
for (const e of events) {
|
||||
switch (e.type) {
|
||||
case "spellCast": {
|
||||
const to = e.targetCell ?? posOf(e.target);
|
||||
const projectile = PROJECTILES[e.cardId];
|
||||
if (projectile && to) {
|
||||
push({ kind: projectile, from: e.from, to });
|
||||
if (projectile === "fireball") push({ kind: "burst", at: to }, 380);
|
||||
if (projectile === "waterbolt") push({ kind: "splash", at: to }, 380);
|
||||
beat++;
|
||||
} else if (to && e.target) {
|
||||
push({ kind: "sparkle", at: to });
|
||||
beat++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "punched":
|
||||
push({ kind: "hit", at: e.at });
|
||||
beat++;
|
||||
break;
|
||||
case "damaged": {
|
||||
const at = posOf(e.player);
|
||||
if (at) { push({ kind: "hit", at }); beat++; }
|
||||
break;
|
||||
}
|
||||
case "attackMissed": {
|
||||
const at = posOf(e.defender);
|
||||
if (at) { push({ kind: "whiff", at }); beat++; }
|
||||
break;
|
||||
}
|
||||
case "attackResolved": {
|
||||
if (e.fullyStopped) {
|
||||
const at = posOf(e.defender);
|
||||
if (at) { push({ kind: "shield", at }); beat++; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "teleported":
|
||||
push({ kind: "shimmer", at: e.from });
|
||||
push({ kind: "shimmer", at: e.to }, 200);
|
||||
beat++;
|
||||
break;
|
||||
case "wallCreated":
|
||||
case "wallDestroyed":
|
||||
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.
|
||||
return out;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user