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 ?? "")}