Targeting dims out-of-reach squares; Speedstone pays out immediately

Selecting an L.O.S. card now drops every square you cannot see into
shadow, leaving eligible targets lit — computed by the engine's own
hasLineOfSight over the player's VIEW, so illusion walls you still
believe in block your aim, stone and bushes and the Big Man block
everyone's, and Around The Corner widens the light one bend when
attached. ADJACENT cards light just your square and its four
neighbors. The shade paints over tokens and wizards alike, and
switches off while arming an ambush, since ambushes aim at the
future. A new sightedCellsFor lives in the engine view module so the
client can never disagree with the rules.

Speedstone, meanwhile, only took effect at next turn's allowance
recompute — displaying it mid-turn granted nothing, as playtesting
found. It now bumps the current allowance by one on display (a delta,
so number-card movement already played survives), unless SLOW or a
zeroed allowance says otherwise. Test covers the immediate grant and
the persistence into later turns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 10:33:02 -04:00
co-authored by Claude Fable 5
parent a0d862ca10
commit 4f3083c490
5 changed files with 111 additions and 5 deletions
+31 -1
View File
@@ -5,7 +5,7 @@
import Help from "./Help.svelte";
import Replay from "./Replay.svelte";
import { local } from "./local.svelte";
import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey, isMovableObject } from "@wizwar/engine";
import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey, isMovableObject, sightedCellsFor } from "@wizwar/engine";
import type { CardInstance, Side } from "@wizwar/engine";
net.connect();
@@ -477,6 +477,35 @@
view.treasures.some((t) => t.position && t.position.x === me.position.x &&
t.position.y === me.position.y && !t.carriedBy),
);
/** Squares a selected L.O.S./ADJACENT card can reach; null = no dimming. */
const litCells = $derived.by(() => {
if (!view || !selectedDef || !isYourTurn) return null;
if (ambushVia || ambushSpell || ambushTrigger) return null; // ambushes aim at the future
if (!me) return null;
if (selectedDef.adjacent === true) {
const { x, y } = me.position;
return new Set(
[[x, y], [x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]
.map(([cx, cy]) => `${cx},${cy}`)
.filter((k) => view.board.cells[k]),
);
}
if (selectedDef.los !== true) return null;
const sighted = sightedCellsFor(view);
if (attachedMods.some((m) => m.cardId === "around-the-corner")) {
const widened = new Set(sighted);
for (const key of Object.keys(view.board.cells)) {
if (widened.has(key)) continue;
const [cx, cy] = key.split(",").map(Number);
if ([[1, 0], [-1, 0], [0, 1], [0, -1]].some(([dx, dy]) => sighted.has(`${cx! + dx!},${cy! + dy!}`))) {
widened.add(key);
}
}
return widened;
}
return sighted;
});
const objectsHere = $derived(
view != null && me != null
? (view.groundObjects[`${me.position.x},${me.position.y}`] ?? [])
@@ -806,6 +835,7 @@
onCreatureClick={clickCreature}
onWarpClick={clickWarp}
markedCell={tradeFrom ?? pendingSectorFrom}
{litCells}
/>
</section>
+22 -1
View File
@@ -15,6 +15,7 @@
onCreatureClick,
onWarpClick,
markedCell = null,
litCells = null,
}: {
view: GameView;
edgeSelectMode?: boolean;
@@ -26,6 +27,8 @@
onWarpClick?: (cell: { x: number; y: number }, side: Side) => void;
/** First square of a two-square spell: marked so the click reads as taken. */
markedCell?: { x: number; y: number } | null;
/** When set, squares NOT in this set dim — the targeting aid. */
litCells?: Set<string> | null;
} = $props();
const PLAYER_COLORS = ["#1a9c46", "#d3352b", "#c9308f", "#3a3ac0", "#2ab0c9", "#c9a72a"];
@@ -431,6 +434,16 @@
onkeydown={() => {}}
/>
{/each}
<!-- targeting aid: ineligible squares fall into shadow -->
{#if litCells}
{#each Object.keys(view.board.cells) as key (key)}
{#if !litCells.has(key)}
{@const dx = Number(key.split(",")[0])}
{@const dy = Number(key.split(",")[1])}
<rect x={dx * CELL} y={dy * CELL} width={CELL} height={CELL} class="dim-cell" />
{/if}
{/each}
{/if}
</svg>
<style>
@@ -519,6 +532,10 @@
fill: #6a5c44;
pointer-events: none;
}
.dim-cell {
fill: rgba(12, 9, 5, 0.55);
pointer-events: none;
}
.marked-cell {
fill: rgba(211, 133, 43, 0.18);
stroke: #d3852b;
@@ -540,7 +557,11 @@
50% { opacity: 0.35; }
}
@media (prefers-reduced-motion: reduce) {
.marked-cell {
.dim-cell {
fill: rgba(12, 9, 5, 0.55);
pointer-events: none;
}
.marked-cell {
fill: rgba(211, 133, 43, 0.18);
stroke: #d3852b;
stroke-width: 2.5;