diff --git a/packages/web/src/FxGallery.svelte b/packages/web/src/FxGallery.svelte index 4a5d716..006dc30 100644 --- a/packages/web/src/FxGallery.svelte +++ b/packages/web/src/FxGallery.svelte @@ -14,9 +14,9 @@ // One representative of every kind, staged on a 3x3 (sectors on a 5x5). const mid = { x: 1, y: 1 }; const SAMPLES: BoardFx[] = ([ - { kind: "fireball", from: { x: 0, y: 1 }, to: { x: 2, y: 1 } }, - { kind: "waterbolt", from: { x: 0, y: 1 }, to: { x: 2, y: 1 } }, - { kind: "bolt", from: { x: 0, y: 0 }, to: { x: 2, y: 2 } }, + { kind: "fireball", a: { x: 24, y: 72 }, b: { x: 120, y: 72 } }, + { kind: "waterbolt", a: { x: 24, y: 72 }, b: { x: 120, y: 72 } }, + { kind: "bolt", a: { x: 24, y: 24 }, b: { x: 120, y: 120 } }, { kind: "burst", at: mid }, { kind: "splash", at: mid }, { kind: "shimmer", at: mid }, @@ -29,7 +29,7 @@ { kind: "absorb", at: mid }, { kind: "portal", cell: mid, side: "E" }, { kind: "portal-cell", at: mid }, - { kind: "streak", from: { x: 0, y: 2 }, to: { x: 2, y: 0 } }, + { kind: "streak", a: { x: 24, y: 120 }, b: { x: 120, y: 24 } }, { kind: "soul", at: { x: 1, y: 2 } }, { kind: "fireworks", at: mid }, { kind: "chaos-swirl", at: mid }, diff --git a/packages/web/src/fx-sprites/Bolt.svelte b/packages/web/src/fx-sprites/Bolt.svelte index 7f96605..2df1f96 100644 --- a/packages/web/src/fx-sprites/Bolt.svelte +++ b/packages/web/src/fx-sprites/Bolt.svelte @@ -1,13 +1,12 @@ diff --git a/packages/web/src/fx-sprites/Streak.svelte b/packages/web/src/fx-sprites/Streak.svelte index 307b902..1307a2d 100644 --- a/packages/web/src/fx-sprites/Streak.svelte +++ b/packages/web/src/fx-sprites/Streak.svelte @@ -1,10 +1,9 @@ diff --git a/packages/web/src/fx-sprites/Waterbolt.svelte b/packages/web/src/fx-sprites/Waterbolt.svelte index 60b2881..f4be33f 100644 --- a/packages/web/src/fx-sprites/Waterbolt.svelte +++ b/packages/web/src/fx-sprites/Waterbolt.svelte @@ -1,12 +1,11 @@ diff --git a/packages/web/src/fx.ts b/packages/web/src/fx.ts index 6cbc1d4..3d8ef11 100644 --- a/packages/web/src/fx.ts +++ b/packages/web/src/fx.ts @@ -5,10 +5,17 @@ import type { GameEvent, GameView } from "@wizwar/engine"; type Cell = { x: number; y: number }; +/** A point in board pixels (travelling effects anchor to token centers). */ +type Pt = { x: number; y: number }; type Side = "N" | "E" | "S" | "W"; +const CELL = 48; +const cellMid = (c: Cell): Pt => ({ x: c.x * CELL + CELL / 2, y: c.y * CELL + CELL / 2 }); +/** Where a wizard token's center sits in a cell (mirrors Board.svelte). */ +const wizMid = (c: Cell): Pt => ({ x: c.x * CELL + CELL / 2, y: c.y * CELL + CELL * 0.36 }); + type FxShape = - | { kind: "fireball" | "bolt" | "waterbolt" | "streak"; from: Cell; to: Cell } + | { kind: "fireball" | "bolt" | "waterbolt" | "streak"; a: Pt; b: Pt } | { kind: "burst" | "splash" | "shimmer" | "shield" | "sparkle" | "whiff" | "hit" | "pow" | "claw" | "absorb" | "portal-cell" | "soul" | "fireworks" | "chaos-swirl" | "pit-fall" | "ooze-slip" | "tacks-ow" | "thorn-snap" | "slime-stuck" | "dust-puff"; at: Cell } @@ -53,6 +60,48 @@ export function fxForEvents( const p = view.players.find((p) => p.id === playerId); return p ? { ...p.position } : null; }; + /** A wizard token's exact center, fan-out included (mirrors Board.svelte). */ + const wizardAnchor = (playerId: string): Pt | null => { + const p = view.players.find((p) => p.id === playerId && p.alive); + if (!p) return null; + const group = view.players.filter( + (q) => q.alive && q.position.x === p.position.x && q.position.y === p.position.y, + ); + const i = group.findIndex((q) => q.id === playerId); + return { + x: p.position.x * CELL + CELL / 2 + (group.length > 1 ? (i - (group.length - 1) / 2) * 14 : 0), + y: p.position.y * CELL + CELL * 0.36, + }; + }; + const creatureAnchor = (creatureId: string): Pt | null => { + const c = view.creatures.find((c) => c.id === creatureId); + if (!c) return null; + const group = view.creatures.filter( + (q) => q.position.x === c.position.x && q.position.y === c.position.y, + ); + const i = group.findIndex((q) => q.id === creatureId); + return { + x: c.position.x * CELL + CELL * 0.72 - (group.length > 1 ? i * CELL * 0.26 : 0), + y: c.position.y * CELL + CELL * 0.7, + }; + }; + /** Best anchor for a spot: the named token if it stands there, else the + * token-height point of the cell, else its plain center. */ + const anchorAt = (cell: Cell, id?: string | null): Pt => { + if (id) { + const a = wizardAnchor(id) ?? creatureAnchor(id); + if (a) return a; + } + const standing = view.players.find( + (p) => p.alive && p.position.x === cell.x && p.position.y === cell.y, + ); + if (standing) return wizardAnchor(standing.id) ?? wizMid(cell); + const crouching = view.creatures.find( + (c) => c.position.x === cell.x && c.position.y === cell.y, + ); + if (crouching) return creatureAnchor(crouching.id) ?? cellMid(cell); + return cellMid(cell); + }; 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 }); @@ -69,7 +118,7 @@ export function fxForEvents( const to = e.targetCell ?? posOf(e.target); const projectile = PROJECTILES[e.cardId]; if (projectile && to) { - push({ kind: projectile, from: e.from, to }); + push({ kind: projectile, a: anchorAt(e.from, e.caster), b: anchorAt(to, e.target) }); if (projectile === "fireball") push({ kind: "burst", at: to }, 380); if (projectile === "waterbolt") push({ kind: "splash", at: to }, 380); beat++; @@ -119,7 +168,7 @@ export function fxForEvents( if ((e.redirected || e.reflectedDamage > 0) && back && stand) { // The spell turns in the air and goes home. const kind = (e.attackCardId && PROJECTILES[e.attackCardId]) || "bolt"; - push({ kind, from: stand, to: back }); + push({ kind, a: anchorAt(stand, e.defender), b: anchorAt(back, e.attacker) }); push({ kind: "hit", at: back }, 380); beat++; } else if (e.fullyStopped && stand) { @@ -177,7 +226,7 @@ export function fxForEvents( } case "washedBack": // The collapsing wave carries them: a streak per victim. - push({ kind: "streak", from: e.from, to: e.to }); + push({ kind: "streak", a: wizMid(e.from), b: anchorAt(e.to, e.player) }); beat++; break; case "died": { @@ -197,15 +246,15 @@ export function fxForEvents( } case "knockedBack": case "shoved": - push({ kind: "streak", from: e.from, to: e.to }); + push({ kind: "streak", a: wizMid(e.from), b: anchorAt(e.to, e.player) }); beat++; break; case "objectDragged": - push({ kind: "streak", from: e.from, to: e.to }); + push({ kind: "streak", a: cellMid(e.from), b: anchorAt(e.to, e.what) }); beat++; break; case "jumpedPit": - push({ kind: "streak", from: e.from, to: e.to }); + push({ kind: "streak", a: wizMid(e.from), b: anchorAt(e.to, e.player) }); beat++; break; case "fellInPit":