A cast that resolved at once draws the sight line it was accepted on
The attack on the stack has always shown its line; a creation or a curse resolved in the same breath showed nothing, so a legal aim through a Visionstone and out a lettered opening (PPL7, move 309) looked like a cheat. The line a cast was accepted on — plain, through the one wall a VISIONSTONE dissolves, or AROUND THE CORNER's two legs — now draws on the live board for a few seconds and on that move of any reel, traced after the fact so the cast's own dust cloud cannot blind it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
12a655868b
commit
7d0c1c68a3
+81
-38
@@ -9,6 +9,7 @@ import {
|
||||
LOS_BLOCKING_CONTENT,
|
||||
type AmbushState,
|
||||
type CastStack,
|
||||
type GameEvent,
|
||||
type PushPending,
|
||||
type CreatureState,
|
||||
type GameState,
|
||||
@@ -328,8 +329,8 @@ function sightBasis(view: GameView): { board: GameView["board"]; blockers: Recor
|
||||
* (believed illusion walls block; held doors admit). Null when no sight
|
||||
* exists — the renderer's material for drawing the line an attack traveled.
|
||||
*/
|
||||
export function traceSightFor(view: GameView, from: Cell, to: Cell): SightTrace | null {
|
||||
if (dustAtEnd(view, from, to)) return null;
|
||||
export function traceSightFor(view: GameView, from: Cell, to: Cell, ignoreDust = false): SightTrace | null {
|
||||
if (!ignoreDust && dustAtEnd(view, from, to)) return null;
|
||||
const { board, blockers } = sightBasis(view);
|
||||
return traceSight(board, from, to, blockers);
|
||||
}
|
||||
@@ -372,54 +373,96 @@ export function bentSightFor(view: GameView, from: Cell, to: Cell): boolean {
|
||||
* defender sharing a square, or no sight under this viewer's knowledge
|
||||
* (a believed illusion wall can honestly hide the line).
|
||||
*/
|
||||
export function stackSightTrace(
|
||||
view: GameView,
|
||||
): {
|
||||
/** A sight line as the board draws it: the legs, the corner bend, and the
|
||||
* one wall a VISIONSTONE dissolved. */
|
||||
export interface SightLine {
|
||||
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 {
|
||||
}
|
||||
|
||||
/** The sight line from one square to another under the sight law the
|
||||
* engine applies to a cast: a plain line (warps included), else the
|
||||
* VISIONSTONE's look through one wall, else AROUND THE CORNER's two legs.
|
||||
* `afterTheFact` traces a cast already resolved, whose own dust cloud
|
||||
* would otherwise blind the line to it. */
|
||||
export function sightTraceBetween(
|
||||
view: GameView,
|
||||
from: Cell,
|
||||
to: Cell,
|
||||
opts: { visionstone?: boolean; bentCorner?: boolean; afterTheFact?: boolean } = {},
|
||||
): SightLine | null {
|
||||
if (from.x === to.x && from.y === to.y) return null;
|
||||
const trace = traceSightFor(view, from, to, opts.afterTheFact);
|
||||
if (trace) return { from, to, 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 (opts.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 }, from, to, blockers);
|
||||
if (t) return { from, to, 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.
|
||||
if (opts.bentCorner) {
|
||||
for (const key of Object.keys(view.board.cells)) {
|
||||
const [mx, my] = key.split(",").map(Number) as [number, number];
|
||||
const mid = { x: mx, y: my };
|
||||
if ((mid.x === from.x && mid.y === from.y) || (mid.x === to.x && mid.y === to.y)) continue;
|
||||
const leg1 = traceSightFor(view, from, mid, opts.afterTheFact);
|
||||
if (!leg1) continue;
|
||||
const leg2 = traceSightFor(view, mid, to, opts.afterTheFact);
|
||||
if (leg2) return { from, to, trace: leg1, bend: { mid, trace: leg2 } };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The sight line of the attack on the stack, for the board to draw. */
|
||||
export function stackSightTrace(view: GameView): SightLine | null {
|
||||
const stack = view.stack;
|
||||
if (!stack || stack.creatureId) return null;
|
||||
if (!stack.attackCard || cardDef(stack.attackCard.cardId).los !== true) return null;
|
||||
const a = view.players.find((p) => p.id === stack.attackerId);
|
||||
const d = view.players.find((p) => p.id === stack.defenderId);
|
||||
if (!a || !d) 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);
|
||||
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.
|
||||
if (stack.bentCorner) {
|
||||
for (const key of Object.keys(view.board.cells)) {
|
||||
const [mx, my] = key.split(",").map(Number) as [number, number];
|
||||
const mid = { x: mx, y: my };
|
||||
if ((mid.x === a.position.x && mid.y === a.position.y) ||
|
||||
(mid.x === d.position.x && mid.y === d.position.y)) continue;
|
||||
const leg1 = traceSightFor(view, a.position, mid);
|
||||
if (!leg1) continue;
|
||||
const leg2 = traceSightFor(view, mid, d.position);
|
||||
if (leg2) return { from: a.position, to: d.position, trace: leg1, bend: { mid, trace: leg2 } };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return sightTraceBetween(view, a.position, d.position, {
|
||||
visionstone: a.displayed.some((c) => c.cardId === "visionstone"),
|
||||
bentCorner: stack.bentCorner === true,
|
||||
});
|
||||
}
|
||||
|
||||
/** The sight line a resolved cast was accepted on — a creation, a curse,
|
||||
* anything aimed by line of sight that never waited on the stack — so the
|
||||
* table can see how the aim was legal. `events` is the cast's own batch,
|
||||
* which names an AROUND THE CORNER cast. */
|
||||
export function castSightTrace(
|
||||
view: GameView,
|
||||
cast: Extract<GameEvent, { type: "spellCast" }>,
|
||||
events: readonly GameEvent[],
|
||||
afterTheFact = false,
|
||||
): SightLine | null {
|
||||
let def;
|
||||
try { def = cardDef(cast.cardId); } catch { return null; }
|
||||
if (def.los !== true) return null;
|
||||
const to = cast.targetCell ?? view.players.find((p) => p.id === cast.target)?.position ?? null;
|
||||
if (!to) return null;
|
||||
const caster = view.players.find((p) => p.id === cast.caster);
|
||||
return sightTraceBetween(view, cast.from, to, {
|
||||
visionstone: caster?.displayed.some((c) => c.cardId === "visionstone") ?? false,
|
||||
bentCorner: events.some((e) => e.type === "castAroundCorner" && e.caster === cast.caster),
|
||||
afterTheFact,
|
||||
});
|
||||
}
|
||||
|
||||
const CREATION_CARD_IDS = new Set([
|
||||
|
||||
Reference in New Issue
Block a user