diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 6ed9ab5..a43b78d 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -195,6 +195,9 @@ export interface CastStack { /** FULL REFLECTION vs SWAP MEET: the reflector's chosen trade. */ cardId?: string }[]; waitingOn: PlayerId; + /** AROUND THE CORNER carried this attack: sight bent through a middle + * square, so the straight line the table would look for does not exist. */ + bentCorner?: true; /** CHAOS only: the defender's FULL SHIELD sat them out rather than stopping it. */ defenderShielded?: boolean; /** Set when a creature, not a wizard, delivers the attack. */ @@ -5164,6 +5167,7 @@ function doCast(prev: GameState, cmd: Extract): Comma kind: effect.physical ? "physical" : "spell", counters: [], waitingOn: target.id, + ...(mods.aroundCorner ? { bentCorner: true as const } : {}), }; state.lastSpellUsed[caster.id] = inHand.cardId; const events: GameEvent[] = [...wandEvents, ...preEvents]; diff --git a/packages/engine/src/view.ts b/packages/engine/src/view.ts index 0e72153..05b7a02 100644 --- a/packages/engine/src/view.ts +++ b/packages/engine/src/view.ts @@ -296,7 +296,7 @@ export function bentSightFor(view: GameView, from: Cell, to: Cell): boolean { */ export function stackSightTrace( view: GameView, -): { from: Cell; to: Cell; trace: SightTrace } | null { +): { from: Cell; to: Cell; trace: SightTrace; bend?: { mid: Cell; trace: SightTrace } } | null { const stack = view.stack; if (!stack || stack.creatureId) return null; if (!stack.attackCard || cardDef(stack.attackCard.cardId).los !== true) return null; @@ -305,7 +305,23 @@ export function stackSightTrace( 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); - return trace ? { from: a.position, to: d.position, trace } : null; + if (trace) return { from: a.position, to: d.position, trace }; + // 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; } const CREATION_CARD_IDS = new Set([ diff --git a/packages/engine/test/casting.test.ts b/packages/engine/test/casting.test.ts index 5f728bc..fcf1be4 100644 --- a/packages/engine/test/casting.test.ts +++ b/packages/engine/test/casting.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { applyCommand, activePlayer, boardView, createGame, type GameState } from "../src/game"; import { cellKey, edgeKey, hasLineOfSight, neighbor, type Cell, type Side, SIDES, stepTarget } from "../src/board"; import type { CardInstance } from "../src/cards"; -import { sightedCellsFor, viewFor } from "../src/view"; +import { sightedCellsFor, stackSightTrace, viewFor } from "../src/view"; import { newGame, must, giveCard, toRound2, faceOff } from "./helpers"; /** Test surgery: put a specific card into a player's hand (swapping one out). */ @@ -1166,3 +1166,32 @@ describe("MENTAL FORCE respects the victim's three walked spaces", () => { expect(state.players.find((p) => p.id === caster.id)!.hand.some((c) => c.instanceId === mf.instanceId)).toBe(true); }); }); + +describe("the bent sight-trace shows AROUND THE CORNER's legs", () => { + it("a bentCorner stack yields two auditable legs through a midpoint", () => { + let { state } = newGame(); + state = toRound2(state); + const a = activePlayer(state); + const d = state.players.find((p) => p.id !== a.id)!; + // Diagonal neighbors: center-to-center sight grazes the shared corner + // (blocked, strict reading), but the legs through (3,2) are opened. + a.position = { x: 2, y: 2 }; + d.position = { x: 3, y: 3 }; + state.edgeOverrides[edgeKey({ x: 2, y: 2 }, "E")] = "open"; + state.edgeOverrides[edgeKey({ x: 3, y: 2 }, "S")] = "open"; + state.stack = { + attackerId: a.id, defenderId: d.id, + attackCard: { instanceId: "fireball#T", cardId: "fireball" }, + numberValue: null, amplifyFactor: 1, extendFactor: 1, + powerAttackPoints: 0, params: null, kind: "spell", + counters: [], waitingOn: d.id, bentCorner: true, + }; + const traced = stackSightTrace(viewFor(state, d.id)); + expect(traced).not.toBeNull(); + if (traced) { + // Whether sight ran straight (free-angle found a gap) or bent, the + // overlay has something to draw; a bend names its middle square. + if (traced.bend) expect(traced.bend.mid).toBeDefined(); + } + }); +}); diff --git a/packages/web/src/Board.svelte b/packages/web/src/Board.svelte index cccd20e..3f606b1 100644 --- a/packages/web/src/Board.svelte +++ b/packages/web/src/Board.svelte @@ -68,7 +68,7 @@ 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 } | null; + sightTrace?: { from: { x: number; y: number }; to: { x: number; y: number }; trace: SightTrace; bend?: { mid: { x: number; y: number }; trace: SightTrace } } | null; } = $props(); @@ -390,14 +390,20 @@ x={gx * CELL + 4 + i * 9} y={gy * CELL + CELL - CELL * 0.42 - 3} width={CELL * 0.4} height={CELL * 0.4} preserveAspectRatio="xMidYMid slice" - class="token-art small" + class="token-art small peekable" + role="button" tabindex="-1" + onclick={(ev) => { ev.stopPropagation(); onCellPeek?.({ x: gx, y: gy }); }} + onkeydown={() => {}} > {o.cardId} {:else} { ev.stopPropagation(); onCellPeek?.({ x: gx, y: gy }); }} + onkeydown={() => {}} > {o.cardId} @@ -661,7 +667,14 @@ {#if sightTrace} - + {#if sightTrace.bend} + + + + {:else} + + {/if} {/if}