From 9493873b641b15533e807821e897decaf1ae2996 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 16 Aug 2026 21:59:18 -0400 Subject: [PATCH] Dispel Creation reaches every creation, and edge hints tell the truth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/engine/src/view.ts | 9 ++++++- .../engine/test/expansion-terrain.test.ts | 22 +++++++++++++++- packages/web/src/App.svelte | 26 ++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/engine/src/view.ts b/packages/engine/src/view.ts index 7a79b62..666924e 100644 --- a/packages/engine/src/view.ts +++ b/packages/engine/src/view.ts @@ -284,7 +284,14 @@ export function eligibleCellsFor(view: GameView, cardId: string): Set | return new Set(cells.filter((k) => view.squareContents[k]?.kind === "stone")); } 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(); + 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; } diff --git a/packages/engine/test/expansion-terrain.test.ts b/packages/engine/test/expansion-terrain.test.ts index b082971..fd04265 100644 --- a/packages/engine/test/expansion-terrain.test.ts +++ b/packages/engine/test/expansion-terrain.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { applyCommand, activePlayer, boardView, gameLos } from "../src/game"; import { cellKey, SIDES, stepTarget, type Cell } from "../src/board"; 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"; describe("expansion terrain", () => { @@ -191,4 +191,24 @@ describe("eligibility dimming mirrors the engine", () => { 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); + }); }); diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 6195616..f538b7a 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -205,6 +205,22 @@ const selectedDef = $derived(selectedCard ? cardDef(selectedCard.cardId) : null); 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 = { + "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([ "create-wall", "destroy-wall", "illusion-wall", "wall-of-fire", "waterwall", "pick-lock", "jam-lock", "remove-lock", "master-key", "dispel-creation", @@ -664,6 +680,12 @@ const creature = view.creatures.find((c) => c.id === creatureId); if (!creature) 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")) { dispatch({ type: "cast", instanceId: selectedCard.instanceId, @@ -1441,7 +1463,9 @@ {/if} {#if selectedDef} {selectedDef.name} - {#if edgeSelectMode}— click a wall line{/if} + {#if selectedCard && EDGE_CARDS.has(selectedCard.cardId)} + — {EDGE_HINTS[selectedCard.cardId] ?? "click a wall line"} + {/if} {#if selectedCard && WAND_IDS.has(selectedCard.cardId) && view.wandCharges[selectedCard.instanceId] == null && !attachedNumber} — first use: tap a NUMBER card to set the wand's charges, then click a target {:else if selectedDef.cardType === "attack" && !cellSelectMode && !EDGE_CARDS.has(selectedCard?.cardId ?? "")}