diff --git a/packages/engine/src/view.ts b/packages/engine/src/view.ts
index 88b28ca..667e1b0 100644
--- a/packages/engine/src/view.ts
+++ b/packages/engine/src/view.ts
@@ -322,7 +322,14 @@ export function bentSightFor(view: GameView, from: Cell, to: Cell): boolean {
*/
export function stackSightTrace(
view: GameView,
-): { from: Cell; to: Cell; trace: SightTrace; bend?: { mid: Cell; trace: SightTrace } } | null {
+): {
+ from: Cell;
+ to: Cell;
+ trace: SightTrace;
+ bend?: { mid: Cell; trace: SightTrace };
+ /** VISIONSTONE: the one wall or door the bearer's sight dissolved (edge key). */
+ pierced?: string;
+} | null {
const stack = view.stack;
if (!stack || stack.creatureId) return null;
if (!stack.attackCard || cardDef(stack.attackCard.cardId).los !== true) return null;
@@ -332,6 +339,19 @@ export function stackSightTrace(
if (a.position.x === d.position.x && a.position.y === d.position.y) return null;
const trace = traceSightFor(view, a.position, d.position);
if (trace) return { from: a.position, to: d.position, trace };
+ // VISIONSTONE: the bearer sees through exactly one wall or door. When no
+ // plain line exists, find the single edge whose removal opens one and draw
+ // the ray straight through it, marking the pierced wall for the table.
+ if (a.displayed.some((c) => c.cardId === "visionstone")) {
+ const { board, blockers } = sightBasis(view);
+ for (const [key, edge] of Object.entries(board.edges)) {
+ if (edge === "open") continue;
+ const edges = { ...board.edges };
+ delete edges[key];
+ const t = traceSight({ ...board, edges }, a.position, d.position, blockers);
+ if (t) return { from: a.position, to: d.position, trace: t, pierced: key };
+ }
+ }
// AROUND THE CORNER: no straight line exists — find a middle square both
// ends can see and draw the sight leg by leg, so the table can audit the
// needle instead of doubting it.
diff --git a/packages/web/src/Board.svelte b/packages/web/src/Board.svelte
index e75dfcd..f8ab810 100644
--- a/packages/web/src/Board.svelte
+++ b/packages/web/src/Board.svelte
@@ -75,9 +75,18 @@
effects?: BoardFx[] | null;
/** The sight line an attack in progress traveled — proof against "how
* can he even see me?", drawn leg by leg through any warp mouth. */
- sightTrace?: { from: { x: number; y: number }; to: { x: number; y: number }; trace: SightTrace; bend?: { mid: { x: number; y: number }; trace: SightTrace } } | null;
+ sightTrace?: { from: { x: number; y: number }; to: { x: number; y: number }; trace: SightTrace; bend?: { mid: { x: number; y: number }; trace: SightTrace }; pierced?: string } | null;
} = $props();
+ /** The wall segment a VISIONSTONE shot pierced, from its edge key. */
+ function piercedSegment(key: string): { x1: number; y1: number; x2: number; y2: number } {
+ const [kind, xy] = key.split(":") as [string, string];
+ const [x, y] = xy.split(",").map(Number) as [number, number];
+ return kind === "V"
+ ? { x1: (x + 1) * CELL, y1: y * CELL, x2: (x + 1) * CELL, y2: (y + 1) * CELL }
+ : { x1: x * CELL, y1: (y + 1) * CELL, x2: (x + 1) * CELL, y2: (y + 1) * CELL };
+ }
+
const SECTOR = 5;
/** Ghost slots can lie beyond the assembled maze — on any side, including
@@ -701,6 +710,11 @@
{:else}
{/if}
+ {#if sightTrace.pierced}
+
+ {@const seg = piercedSegment(sightTrace.pierced)}
+
+ {/if}
{/if}
@@ -804,6 +818,13 @@
stroke-width: 2;
animation: sight-pulse 1.6s ease-in-out infinite;
}
+ .sight-pierced {
+ stroke: #c9a72a;
+ stroke-width: 5;
+ stroke-linecap: round;
+ stroke-dasharray: 2 5;
+ animation: sight-pulse 1.6s ease-in-out infinite;
+ }
@keyframes sight-pulse {
0%, 100% { opacity: 0.9; }
50% { opacity: 0.35; }