Travelling effects fly token to token
Projectiles flew cell-center to cell-center, launching from the caster's feet and landing under the target's chin — tokens sit high in their squares and fan sideways when crowded. Every travelling effect (fireball, waterbolt, bolt, streak, and the reflection's return flight) now resolves token anchors that mirror the board's exact positioning, fan-out included: caster's center to target's center, wizard or creature. Spots with no token to aim at fall back to sensible cell points, and wash/knockback streaks end on the carried wizard wherever they fanned to. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
126f003156
commit
93bdbb403a
@@ -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 },
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"bolt"> } = $props();
|
||||
const uid = $props.id();
|
||||
|
||||
/** A fresh jagged path every strike, plus two forks off mid-joints. */
|
||||
const geom = $derived.by(() => {
|
||||
const a = center(fx.from), b = center(fx.to);
|
||||
const a = fx.a, b = fx.b;
|
||||
const segs = 6;
|
||||
const joints: { x: number; y: number }[] = [a];
|
||||
for (let i = 1; i < segs; i++) {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"fireball"> } = $props();
|
||||
const uid = $props.id();
|
||||
|
||||
const a = $derived(center(fx.from));
|
||||
const b = $derived(center(fx.to));
|
||||
const a = $derived(fx.a);
|
||||
const b = $derived(fx.b);
|
||||
</script>
|
||||
|
||||
<defs>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"streak"> } = $props();
|
||||
const a = $derived(center(fx.from));
|
||||
const b = $derived(center(fx.to));
|
||||
const a = $derived(fx.a);
|
||||
const b = $derived(fx.b);
|
||||
const d = $derived(`M ${a.x} ${a.y} L ${b.x} ${b.y}`);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"waterbolt"> } = $props();
|
||||
const uid = $props.id();
|
||||
|
||||
const a = $derived(center(fx.from));
|
||||
const b = $derived(center(fx.to));
|
||||
const a = $derived(fx.a);
|
||||
const b = $derived(fx.b);
|
||||
</script>
|
||||
|
||||
<defs>
|
||||
|
||||
+56
-7
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user