From 773f0a7d212d168e4dd07755c3963148680c9492 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Thu, 27 Aug 2026 18:52:46 -0400 Subject: [PATCH] Visionstone shots draw their ray and mark the pierced wall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A stone-bearing automaton fired Blind through a warp mouth and one wall, and the table saw nothing but the attack modal — the sight trace only knew plain and bent-corner lines, so a legal shot read as a cheat. When the attacker displays VISIONSTONE and no plain line exists, stackSightTrace now finds the single edge whose removal opens the line (warp legs included), draws the ray through it, and the keymap pulses the dissolved wall so the answer to "how can he even see me?" is on the board. Verified against the live LZ5R blind moment: warp mouths at (0,2)W and (4,7)E, pierced wall V:3,8. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF --- packages/engine/src/view.ts | 22 +++++++++++++++++++++- packages/web/src/Board.svelte | 23 ++++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) 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}