Visionstone shots draw their ray and mark the pierced wall

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-27 18:52:46 -04:00
co-authored by Claude Fable 5
parent 358633022c
commit 773f0a7d21
2 changed files with 43 additions and 2 deletions
+21 -1
View File
@@ -322,7 +322,14 @@ export function bentSightFor(view: GameView, from: Cell, to: Cell): boolean {
*/ */
export function stackSightTrace( export function stackSightTrace(
view: GameView, 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; const stack = view.stack;
if (!stack || stack.creatureId) return null; if (!stack || stack.creatureId) return null;
if (!stack.attackCard || cardDef(stack.attackCard.cardId).los !== true) 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; if (a.position.x === d.position.x && a.position.y === d.position.y) return null;
const trace = traceSightFor(view, a.position, d.position); const trace = traceSightFor(view, a.position, d.position);
if (trace) return { from: a.position, to: d.position, trace }; 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 // 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 // ends can see and draw the sight leg by leg, so the table can audit the
// needle instead of doubting it. // needle instead of doubting it.
+22 -1
View File
@@ -75,9 +75,18 @@
effects?: BoardFx[] | null; effects?: BoardFx[] | null;
/** The sight line an attack in progress traveled — proof against "how /** The sight line an attack in progress traveled — proof against "how
* can he even see me?", drawn leg by leg through any warp mouth. */ * 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(); } = $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; const SECTOR = 5;
/** Ghost slots can lie beyond the assembled maze — on any side, including /** Ghost slots can lie beyond the assembled maze — on any side, including
@@ -701,6 +710,11 @@
{:else} {:else}
<SightTraceOverlay from={sightTrace.from} to={sightTrace.to} trace={sightTrace.trace} /> <SightTraceOverlay from={sightTrace.from} to={sightTrace.to} trace={sightTrace.trace} />
{/if} {/if}
{#if sightTrace.pierced}
<!-- the one wall VISIONSTONE dissolved for this shot -->
{@const seg = piercedSegment(sightTrace.pierced)}
<line x1={seg.x1} y1={seg.y1} x2={seg.x2} y2={seg.y2} class="sight-pierced" />
{/if}
{/if} {/if}
<!-- spell flourishes: one sprite component per effect (src/fx-sprites/) --> <!-- spell flourishes: one sprite component per effect (src/fx-sprites/) -->
<g class="fx-layer" aria-hidden="true"> <g class="fx-layer" aria-hidden="true">
@@ -804,6 +818,13 @@
stroke-width: 2; stroke-width: 2;
animation: sight-pulse 1.6s ease-in-out infinite; 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 { @keyframes sight-pulse {
0%, 100% { opacity: 0.9; } 0%, 100% { opacity: 0.9; }
50% { opacity: 0.35; } 50% { opacity: 0.35; }