Idiot enforces its card (rev 23) + attack sight-line tracing

Rules rev 23 — IDIOT, per the card and FAQ:
- No handling items: pick up / drop of treasures and objects refused
  (dropping was the exploit: capturing a stolen treasure on your own home,
  or dropping your own treasure underfoot for an instant cure)
- No punching, no thrown dagger / large rock (attacks on players)
- No effect on a victim already carrying one of their own treasures
Ungated (permissive): counteractions are now castable while idiotized (the
one thing the card expressly allows — the gate wrongly blocked them), and
goal-aiding spells (IDIOT_AIDS: destroy-wall, teleport, mad-dash, ...) per
the FAQ's 'you could, however, destroy a wall'.

Sight tracing: while an LOS attack sits on the stack, the board draws the
line it traveled — straight when direct, leg by leg through both warp
mouths (with pulsing rings) when the maze's wraparound carried it. Engine
traceSight/traceSightFor/stackSightTrace; overlay in Board.svelte; shown
live and in replays. Verified against H3PC's cross-board Idiot cast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-17 19:03:09 -04:00
co-authored by Claude Fable 5
parent 7372bfc5e8
commit cdeb3d5103
10 changed files with 290 additions and 24 deletions
+6 -1
View File
@@ -10,7 +10,7 @@
import { scheduleFx, type BoardFx } from "./fx";
import { CREATURE_ART, objectArt, TERRAIN_ART, tokenArt } from "./art";
import { local } from "./local.svelte";
import { allCardDefs, cardDef, isNumberCard, isPermanentDuration, SIDES, stepTarget, cellKey, isMovableObject, sightedCellsFor, type GameView, eligibleCellsFor } from "@wizwar/engine";
import { allCardDefs, cardDef, isNumberCard, isPermanentDuration, SIDES, stepTarget, cellKey, isMovableObject, sightedCellsFor, stackSightTrace, type GameView, eligibleCellsFor } from "@wizwar/engine";
import type { CardInstance, Side } from "@wizwar/engine";
net.connect();
@@ -854,6 +854,10 @@
t.position.y === me.position.y && !t.carriedBy),
);
/** Squares a selected L.O.S./ADJACENT card can reach; null = no dimming. */
// While an LOS attack sits on the stack, draw the line it traveled — the
// answer to "how can he even see me?" when sight ran through a warp mouth.
const sightTrace = $derived(view ? stackSightTrace(view) : null);
const litCells = $derived.by(() => {
if (!view || !selectedDef || !yourMoment) return null;
if (ambushVia || ambushSpell || ambushTrigger) return null; // ambushes aim at the future
@@ -1386,6 +1390,7 @@
onGhostClick={clickGhostSlot}
onCellPeek={(c) => peekAt(c)}
{litCells}
{sightTrace}
/>
</section>
+41 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Component } from "svelte";
import type { GameView } from "@wizwar/engine";
import type { GameView, SightTrace } from "@wizwar/engine";
import type { Side } from "@wizwar/engine";
import type { BoardFx } from "./fx";
import { colorIndexOf as sharedColorIndex, wizardColor } from "./colors";
@@ -28,6 +28,7 @@
ghostSlots = null,
onGhostClick,
effects = null,
sightTrace = null,
}: {
view: GameView;
edgeSelectMode?: boolean;
@@ -54,6 +55,9 @@
onGhostClick?: (origin: { x: number; y: number }) => void;
/** Short-lived spell flourishes; purely cosmetic. */
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;
} = $props();
@@ -601,6 +605,23 @@
{/if}
{/each}
{/if}
<!-- the sight line an attack traveled, leg by leg through any warp mouth -->
{#if sightTrace}
{@const A = { x: sightTrace.from.x * CELL + CELL / 2, y: sightTrace.from.y * CELL + CELL * 0.36 }}
{@const B = { x: sightTrace.to.x * CELL + CELL / 2, y: sightTrace.to.y * CELL + CELL * 0.36 }}
<g class="sight-layer" aria-hidden="true">
{#if sightTrace.trace.kind === "direct"}
<line x1={A.x} y1={A.y} x2={B.x} y2={B.y} class="sight-line" />
{:else}
{@const entry = { x: sightTrace.trace.entry.x * CELL, y: sightTrace.trace.entry.y * CELL }}
{@const exit = { x: sightTrace.trace.exit.x * CELL, y: sightTrace.trace.exit.y * CELL }}
<line x1={A.x} y1={A.y} x2={entry.x} y2={entry.y} class="sight-line" />
<line x1={exit.x} y1={exit.y} x2={B.x} y2={B.y} class="sight-line" />
<circle cx={entry.x} cy={entry.y} r={7} class="sight-mouth" />
<circle cx={exit.x} cy={exit.y} r={7} class="sight-mouth" />
{/if}
</g>
{/if}
<!-- spell flourishes: one sprite component per effect (src/fx-sprites/) -->
<g class="fx-layer" aria-hidden="true">
{#each effects ?? [] as fx (fx.id)}
@@ -757,6 +778,24 @@
.ghost-slot:hover { fill: rgba(122, 162, 122, 0.22); }
.fx-layer { pointer-events: none; }
.sight-layer { pointer-events: none; }
.sight-line {
stroke: #c9a72a;
stroke-width: 2.5;
stroke-linecap: round;
stroke-dasharray: 7 6;
opacity: 0.85;
animation: sight-march 0.8s linear infinite;
}
.sight-mouth {
fill: none;
stroke: #c9a72a;
stroke-width: 2;
animation: warp-pulse 1.6s ease-in-out infinite;
}
@keyframes sight-march {
to { stroke-dashoffset: -13; }
}
.mover { transition: transform 260ms cubic-bezier(0.25, 0.8, 0.35, 1); }
.mover.snap { transition: none; }
@@ -783,6 +822,7 @@
}
@media (prefers-reduced-motion: reduce) {
.marked-cell, .marked-sector, .ghost-slot, .warp-dest { animation: none; }
.sight-line, .sight-mouth { animation: none; }
.fx-layer { display: none; }
.firewall, :global(.token-art.ghosted) { animation: none; }
.mover { transition: none; }
+6 -1
View File
@@ -2,6 +2,7 @@
import Board from "./Board.svelte";
import { humanize } from "./net.svelte";
import { scheduleFx, type BoardFx } from "./fx";
import { stackSightTrace } from "@wizwar/engine";
import type { GameEvent, GameView } from "@wizwar/engine";
let {
@@ -21,6 +22,10 @@
);
const atEnd = $derived(idx >= steps.length - 1);
// The reel draws the same sight line the live table shows for an LOS
// attack in progress, so a replay-watcher can see how a spell reached them.
const sightTrace = $derived(stackSightTrace(step.view));
/** Each step's spells flare on the reel exactly as they did at the table. */
let boardFx = $state<BoardFx[]>([]);
$effect(() => {
@@ -61,7 +66,7 @@
<button class="replay-skip" onclick={onclose}>{atEnd ? "back to the game" : "skip to now"}</button>
</header>
<div class="replay-board">
<Board view={step.view} effects={boardFx} />
<Board view={step.view} effects={boardFx} {sightTrace} />
</div>
<div class="replay-caption">
<strong>{step.actor}</strong>
+1 -1
View File
@@ -136,7 +136,7 @@ class LocalGame {
seed,
sets: expansion ? ["basic", "expansion1"] : ["basic"],
...(colors ? { colors } : {}),
deckRev: 22,
deckRev: 23,
};
const { state, events } = createGame(config);
for (const e of events) {