Expansion wave 3: twelve terrain cards

KILLER OOZE (1 damage on entry, slip on 1-2: drop treasure, 2 more,
flat on your face until you roll to stand), CREATE PIT (jumps roll a
D4 — clear it on 2-4, fall in on a 1 and climb out on later rolls),
ROSEBUSH (3-point passage, blocks sight), DUST CLOUD (a permanent
zone of blindness: direction rolls inside, no LOS through, defeats
Visionstone), FILL SQUARE WITH SLIME (entering ends your actions,
blocks sight), HANDFUL OF TACKS (adjacent-only scatter, 3 points to
cross), CREATE DOOR (a new locked door in any wall or corridor),
BOOBYTRAP (four face-down tokens, one secretly real — 4 points under
anyone but the caster; the caster's view alone marks the true token),
GLUE (objects pinned for twice the number card), SAFE (items locked
away from everyone but the creator), TRADER (swap two floor items in
sight; glue and safes hold fast), and STONE TO WATER (walls melt into
range-2 waves, stone blocks into range-4 bursts). LOS blocking is now
per-terrain-kind. The board renders all of it, boobytrap placement is
a four-click ritual, and Trader is two. 108 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-15 22:52:34 -04:00
co-authored by Claude Fable 5
parent 28949bdb7d
commit 3a35922a40
5 changed files with 689 additions and 22 deletions
+32 -1
View File
@@ -29,6 +29,10 @@
let attachedMods = $state<CardInstance[]>([]);
/** relocate-sector: the sector picked up, awaiting its destination. */
let pendingSectorFrom = $state<{ x: number; y: number } | null>(null);
/** boobytrap: cells picked so far (first is the real one). */
let trapCells = $state<{ x: number; y: number }[]>([]);
/** trader: first item square picked. */
let tradeFrom = $state<{ x: number; y: number } | null>(null);
/** rotate-sector direction. */
let rotateCW = $state(true);
/** Cards marked for discard. */
@@ -43,12 +47,14 @@
const EDGE_CARDS = new Set([
"create-wall", "destroy-wall", "wall-of-fire", "waterwall",
"pick-lock", "jam-lock", "remove-lock", "master-key", "dispel-creation",
"warp-wand",
"warp-wand", "create-door", "stone-to-water",
]);
const CELL_CARDS = new Set([
"teleport", "fill-square-with-stone", "thornbush", "dispel-creation", "drag",
"rotate-sector", "relocate-sector",
"troll", "skeleton", "wraith", "fire-imp", "democratic-monster", "shadow",
"killer-ooze", "rosebush", "dust-cloud", "fill-square-with-slime", "create-pit",
"handful-of-tacks", "glue", "safe", "trader", "stone-to-water", "boobytrap",
]);
const CREATURE_TARGET_CARDS = new Set(["mega-monster"]);
const MODIFIER_CARDS = new Set(["amplify", "add", "extend", "around-the-corner"]);
@@ -63,6 +69,8 @@
const numberTotal = $derived(attachedNumber ? cardDef(attachedNumber.cardId).value! : 1);
function clearSelection() {
trapCells = [];
tradeFrom = null;
selectedCreature = null;
selectedCard = null;
attachedNumber = null;
@@ -181,6 +189,23 @@
clearSelection();
return;
}
if (selectedCard?.cardId === "boobytrap") {
trapCells = [...trapCells, cell];
if (trapCells.length === 4) {
net.command({ type: "cast", instanceId: selectedCard.instanceId, params: { cells: trapCells } });
clearSelection();
}
return;
}
if (selectedCard?.cardId === "trader") {
if (!tradeFrom) { tradeFrom = cell; return; }
net.command({
type: "cast", instanceId: selectedCard.instanceId,
target: { kind: "cell", cell }, params: { cell: tradeFrom },
});
clearSelection();
return;
}
if (selectedCard?.cardId === "rotate-sector") {
net.command({
type: "cast", instanceId: selectedCard.instanceId,
@@ -457,6 +482,12 @@
<label><input type="checkbox" bind:checked={rotateCW} /> clockwise</label>
— click a square in the sector to rotate
{/if}
{#if selectedCard?.cardId === "boobytrap"}
— place 4 tokens ({trapCells.length}/4; the FIRST is the real trap)
{/if}
{#if selectedCard?.cardId === "trader"}
{tradeFrom ? "— now the second item square" : "— click the first item square"}
{/if}
{#if selectedCard?.cardId === "relocate-sector"}
{#if pendingSectorFrom}
— now click the destination area