diff --git a/packages/web/src/Board.svelte b/packages/web/src/Board.svelte index fe210f2..b263580 100644 --- a/packages/web/src/Board.svelte +++ b/packages/web/src/Board.svelte @@ -662,10 +662,16 @@ {@const c = center(fx.at)} - {:else if fx.kind === "warp-glow"} - {@const c = center(fx.at)} - - + {:else if fx.kind === "portal"} + {@const x1 = fx.side === "E" ? (fx.cell.x + 1) * CELL : fx.cell.x * CELL} + {@const y1 = fx.side === "S" ? (fx.cell.y + 1) * CELL : fx.cell.y * CELL} + {@const x2 = fx.side === "W" ? fx.cell.x * CELL : (fx.cell.x + 1) * CELL} + {@const y2 = fx.side === "N" ? fx.cell.y * CELL : (fx.cell.y + 1) * CELL} + + + {:else if fx.kind === "portal-cell"} + {:else if fx.kind === "streak"} {@const a = center(fx.from)} {@const b = center(fx.to)} @@ -989,16 +995,40 @@ stroke-width: 1.5; animation: fx-swallow 0.65s ease-in forwards; } - .fx-warpglow { - transform-box: fill-box; - transform-origin: center; - fill: none; - stroke: #2e7d32; + .fx-portal { + stroke: #9be3e0; stroke-width: 3; - filter: drop-shadow(0 0 5px rgba(46, 125, 50, 0.8)); - animation: fx-ring 0.55s ease-out forwards; + stroke-dasharray: 6 5; + stroke-linecap: round; + filter: drop-shadow(0 0 4px rgba(155, 227, 224, 0.9)); + animation: fx-portal-shimmer 0.95s ease-in-out forwards; + } + .fx-portal.glow { + stroke: rgba(180, 138, 224, 0.5); + stroke-width: 9; + stroke-dasharray: none; + filter: blur(2px); + animation: fx-portal-breathe 0.95s ease-in-out forwards; + } + .fx-portal-veil { + fill: rgba(155, 227, 224, 0.12); + stroke: #9be3e0; + stroke-width: 2.5; + stroke-dasharray: 7 5; + filter: drop-shadow(0 0 4px rgba(155, 227, 224, 0.8)); + animation: fx-portal-shimmer 0.95s ease-in-out forwards; + } + @keyframes fx-portal-shimmer { + 0% { opacity: 0; stroke-dashoffset: 0; } + 20% { opacity: 1; } + 80% { opacity: 0.85; } + 100% { opacity: 0; stroke-dashoffset: 34; } + } + @keyframes fx-portal-breathe { + 0% { opacity: 0; } + 30% { opacity: 0.8; } + 100% { opacity: 0; } } - .fx-warpglow.late { stroke: #7ac47e; animation-delay: 0.15s; opacity: 0; } .fx-streak { stroke: rgba(233, 225, 203, 0.85); stroke-width: 5; diff --git a/packages/web/src/fx.ts b/packages/web/src/fx.ts index 2c09ae5..8190075 100644 --- a/packages/web/src/fx.ts +++ b/packages/web/src/fx.ts @@ -10,8 +10,9 @@ type Side = "N" | "E" | "S" | "W"; type FxShape = | { kind: "fireball" | "bolt" | "waterbolt" | "streak"; from: Cell; to: Cell } | { kind: "burst" | "splash" | "shimmer" | "shield" | "sparkle" | "whiff" | "hit" - | "pow" | "claw" | "absorb" | "warp-glow" | "soul" | "fireworks" | "chaos-swirl" + | "pow" | "claw" | "absorb" | "portal-cell" | "soul" | "fireworks" | "chaos-swirl" | "pit-fall" | "ooze-slip" | "tacks-ow" | "thorn-snap" | "slime-stuck" | "dust-puff"; at: Cell } + | { kind: "portal"; cell: Cell; side: Side } | { kind: "sector-spin"; origin: Cell; clockwise: boolean } | { kind: "sector-slide"; from: Cell; to: Cell } | { kind: "edge-dust"; cell: Cell; side: Side }; @@ -24,6 +25,7 @@ export function fxTtl(kind: BoardFx["kind"]): number { switch (kind) { case "fireball": case "waterbolt": return 700; case "bolt": case "streak": return 600; + case "portal": case "portal-cell": case "soul": case "fireworks": case "chaos-swirl": case "sector-spin": case "sector-slide": return 1500; default: return 900; @@ -131,14 +133,23 @@ export function fxForEvents( break; case "moved": if (e.via === "warp") { - push({ kind: "warp-glow", at: e.from }); - push({ kind: "warp-glow", at: e.to }, 150); + // The mouths are edges: shimmer the opening lines on both boards. + const w = view.board.warps.find((w) => + w.from.cell.x === e.from.x && w.from.cell.y === e.from.y && w.from.side === e.direction); + if (w) { + push({ kind: "portal", cell: w.from.cell, side: w.from.side }); + push({ kind: "portal", cell: w.to.cell, side: w.to.side }, 150); + } else { + push({ kind: "portal-cell", at: e.from }); + push({ kind: "portal-cell", at: e.to }, 150); + } beat++; } break; case "warpStepped": - push({ kind: "warp-glow", at: e.from }); - push({ kind: "warp-glow", at: e.to }, 150); + // Dimensional warp tokens fill their squares; the veil wraps them. + push({ kind: "portal-cell", at: e.from }); + push({ kind: "portal-cell", at: e.to }, 150); beat++; break; case "wallCreated":