Dispel Creation reaches every creation, and edge hints tell the truth

An opponent's wraith refused to be dispelled — the engine was willing
all along (any controller's creature, sight required), but creature
clicks never routed cell-target spells, so the cast never left the
client. Clicking a creature with a cell spell selected now lands on
its square. The dimming aid told the same lie twice over: it lit
every created square including ones out of sight (a caster's own
thornbush beyond the walls) and never lit creatures at all. It now
mirrors the engine — anything created, whoever made it, in sight.

And "click a wall line" stops standing in for every edge card: each
now says what the click actually is — a locked door for the keys, a
corridor line for the fire, the creation itself for the dispel — and
attacks no longer show two hints for one wall.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 21:59:18 -04:00
co-authored by Claude Fable 5
parent ee6a08f25c
commit 9493873b64
3 changed files with 54 additions and 3 deletions
+8 -1
View File
@@ -284,7 +284,14 @@ export function eligibleCellsFor(view: GameView, cardId: string): Set<string> |
return new Set(cells.filter((k) => view.squareContents[k]?.kind === "stone")); return new Set(cells.filter((k) => view.squareContents[k]?.kind === "stone"));
} }
if (cardId === "dispel-creation") { if (cardId === "dispel-creation") {
return new Set(cells.filter((k) => view.squareContents[k] != null)); // Anything created — terrain or creature, whoever made it — in sight.
const out = new Set<string>();
for (const k of cells) {
const created = view.squareContents[k] != null ||
view.creatures.some((c) => key(c.position.x, c.position.y) === k);
if (created && sighted.has(k)) out.add(k);
}
return out;
} }
return null; return null;
} }
+21 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { applyCommand, activePlayer, boardView, gameLos } from "../src/game"; import { applyCommand, activePlayer, boardView, gameLos } from "../src/game";
import { cellKey, SIDES, stepTarget, type Cell } from "../src/board"; import { cellKey, SIDES, stepTarget, type Cell } from "../src/board";
import type { CardInstance } from "../src/cards"; import type { CardInstance } from "../src/cards";
import { eligibleCellsFor, viewFor } from "../src/view"; import { eligibleCellsFor, sightedCellsFor, viewFor } from "../src/view";
import { newExpansionGame as newGame, must, giveCard, emptyNeighborCell } from "./helpers"; import { newExpansionGame as newGame, must, giveCard, emptyNeighborCell } from "./helpers";
describe("expansion terrain", () => { describe("expansion terrain", () => {
@@ -191,4 +191,24 @@ describe("eligibility dimming mirrors the engine", () => {
expect(held).toBe(true); expect(held).toBe(true);
} }
}); });
it("dispel dimming lights creatures and demands sight, whoever made them", () => {
let { state } = newGame();
const caster = activePlayer(state);
const other = state.players.find((p) => p.id !== caster.id)!;
// An enemy wraith beside the caster: created by the OTHER wizard.
state.creatures.push({
id: "w1", kind: "wraith", controllerId: other.id,
position: { x: caster.position.x, y: caster.position.y },
damage: 0, maxDamage: Infinity, movesPerTurn: 3, movementUsed: 0,
attackUsed: false, justCreated: false,
wallPassesPerTurn: 1, wallPassUsed: 0, scorchedThisTurn: [],
});
const lit = eligibleCellsFor(viewFor(state, caster.id), "dispel-creation")!;
expect(lit.has(cellKey(caster.position))).toBe(true);
// Everything lit is created AND sighted.
const view = viewFor(state, caster.id);
const seen = sightedCellsFor(view);
for (const k of lit) expect(seen.has(k)).toBe(true);
});
}); });
+25 -1
View File
@@ -205,6 +205,22 @@
const selectedDef = $derived(selectedCard ? cardDef(selectedCard.cardId) : null); const selectedDef = $derived(selectedCard ? cardDef(selectedCard.cardId) : null);
const WAND_IDS = new Set(["blaster-wand", "sticky-wand", "shift-wand", "warp-wand"]); const WAND_IDS = new Set(["blaster-wand", "sticky-wand", "shift-wand", "warp-wand"]);
/** What the edge click actually is, per card — "wall line" fits few. */
const EDGE_HINTS: Record<string, string> = {
"create-wall": "click an open corridor line to wall it",
"destroy-wall": "click a wall or door to destroy it",
"illusion-wall": "click an open corridor line for the false wall",
"wall-of-fire": "click a corridor line for the fire",
"waterwall": "click a corridor line — the wave collapses at once",
"pick-lock": "click a locked door",
"master-key": "click a locked door",
"jam-lock": "click a door to jam its lock solid",
"remove-lock": "click a door to strip its lock for good",
"create-door": "click a wall for the new door",
"warp-wand": "click a wall to warp it open",
"stone-to-water": "click a stone wall, or a stone-filled square",
"dispel-creation": "click the creation — a square, a creature, or a conjured wall",
};
const EDGE_CARDS = new Set([ const EDGE_CARDS = new Set([
"create-wall", "destroy-wall", "illusion-wall", "wall-of-fire", "waterwall", "create-wall", "destroy-wall", "illusion-wall", "wall-of-fire", "waterwall",
"pick-lock", "jam-lock", "remove-lock", "master-key", "dispel-creation", "pick-lock", "jam-lock", "remove-lock", "master-key", "dispel-creation",
@@ -664,6 +680,12 @@
const creature = view.creatures.find((c) => c.id === creatureId); const creature = view.creatures.find((c) => c.id === creatureId);
if (!creature) return; if (!creature) return;
if (!isYourTurn) { peekAt(creature.position); return; } if (!isYourTurn) { peekAt(creature.position); return; }
if (selectedCard && CELL_CARDS.has(selectedCard.cardId)) {
// Cell-target spells aimed at a creature (DISPEL CREATION on a wraith)
// land on its square — the engine finds the creature there.
clickCell(creature.position);
return;
}
if (selectedCard && (CREATURE_TARGET_CARDS.has(selectedCard.cardId) || cardDef(selectedCard.cardId).cardType === "attack")) { if (selectedCard && (CREATURE_TARGET_CARDS.has(selectedCard.cardId) || cardDef(selectedCard.cardId).cardType === "attack")) {
dispatch({ dispatch({
type: "cast", instanceId: selectedCard.instanceId, type: "cast", instanceId: selectedCard.instanceId,
@@ -1441,7 +1463,9 @@
{/if} {/if}
{#if selectedDef} {#if selectedDef}
<strong class="hint-name">{selectedDef.name}</strong> <strong class="hint-name">{selectedDef.name}</strong>
{#if edgeSelectMode}<span>— click a wall line</span>{/if} {#if selectedCard && EDGE_CARDS.has(selectedCard.cardId)}
<span>— {EDGE_HINTS[selectedCard.cardId] ?? "click a wall line"}</span>
{/if}
{#if selectedCard && WAND_IDS.has(selectedCard.cardId) && view.wandCharges[selectedCard.instanceId] == null && !attachedNumber} {#if selectedCard && WAND_IDS.has(selectedCard.cardId) && view.wandCharges[selectedCard.instanceId] == null && !attachedNumber}
<span>— first use: tap a NUMBER card to set the wand's charges, then click a target</span> <span>— first use: tap a NUMBER card to set the wand's charges, then click a target</span>
{:else if selectedDef.cardType === "attack" && !cellSelectMode && !EDGE_CARDS.has(selectedCard?.cardId ?? "")} {:else if selectedDef.cardType === "attack" && !cellSelectMode && !EDGE_CARDS.has(selectedCard?.cardId ?? "")}