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:
co-authored by
Claude Fable 5
parent
28949bdb7d
commit
3a35922a40
@@ -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
|
||||
|
||||
@@ -111,11 +111,31 @@
|
||||
{@const sy = Number(key.split(",")[1])}
|
||||
{#if content.kind === "stone"}
|
||||
<rect x={sx * CELL + 2} y={sy * CELL + 2} width={CELL - 4} height={CELL - 4} class="stone" rx="4" />
|
||||
{:else}
|
||||
<circle cx={sx * CELL + CELL / 2} cy={sy * CELL + CELL / 2} r={CELL * 0.36} class="bush" />
|
||||
{:else if content.kind === "thornbush" || content.kind === "rosebush"}
|
||||
<circle cx={sx * CELL + CELL / 2} cy={sy * CELL + CELL / 2} r={CELL * 0.36}
|
||||
class={content.kind === "rosebush" ? "rose" : "bush"} />
|
||||
{:else if content.kind === "ooze" || content.kind === "slime"}
|
||||
<rect x={sx * CELL + 5} y={sy * CELL + 5} width={CELL - 10} height={CELL - 10} rx="10"
|
||||
class={content.kind === "ooze" ? "ooze" : "slime"} />
|
||||
{:else if content.kind === "dust"}
|
||||
<circle cx={sx * CELL + CELL / 2} cy={sy * CELL + CELL / 2} r={CELL * 0.4} class="dust" />
|
||||
{:else if content.kind === "pit"}
|
||||
<rect x={sx * CELL + 7} y={sy * CELL + 7} width={CELL - 14} height={CELL - 14} class="pit" />
|
||||
{:else if content.kind === "tacks"}
|
||||
<text x={sx * CELL + CELL / 2} y={sy * CELL + CELL / 2 + 4} class="tacks">✻✻</text>
|
||||
{:else if content.kind === "safe"}
|
||||
<rect x={sx * CELL + 10} y={sy * CELL + 12} width={CELL - 20} height={CELL - 22} rx="3" class="safe" />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<!-- boobytrap tokens: face-down for everyone (the caster knows the real one) -->
|
||||
{#each view.boobytraps as trap, ti (ti)}
|
||||
{#each trap.cells as tc, i (i)}
|
||||
<circle cx={tc.x * CELL + CELL * 0.26} cy={tc.y * CELL + CELL * 0.26} r="6"
|
||||
class="trap-token" class:trap-real={trap.realCell != null && tc.x === trap.realCell.x && tc.y === trap.realCell.y} />
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
<!-- ground objects -->
|
||||
{#each Object.entries(view.groundObjects) as [key, objects] (key)}
|
||||
{@const gx = Number(key.split(",")[0])}
|
||||
@@ -245,6 +265,15 @@
|
||||
.firewall { fill: #e0442a; }
|
||||
.stone { fill: #6a6458; stroke: #3a362e; stroke-width: 2; }
|
||||
.bush { fill: #2e7d32; stroke: #1b4d1e; stroke-width: 2; }
|
||||
.rose { fill: #2e7d32; stroke: #b0245a; stroke-width: 3; }
|
||||
.ooze { fill: #58a12b; opacity: 0.85; }
|
||||
.slime { fill: #8ec52e; opacity: 0.85; }
|
||||
.dust { fill: #9b9184; opacity: 0.75; }
|
||||
.pit { fill: #171512; }
|
||||
.tacks { font-size: 15px; text-anchor: middle; fill: #444; }
|
||||
.safe { fill: #7d8894; stroke: #2f3844; stroke-width: 2; }
|
||||
.trap-token { fill: #513c22; stroke: #201709; stroke-width: 1.5; }
|
||||
.trap-real { stroke: #d3352b; stroke-width: 2.5; }
|
||||
.ground-object { fill: #a6812e; stroke: #4a3a10; stroke-width: 1; }
|
||||
.illusion { stroke: #7a6f9a; stroke-width: 4; stroke-dasharray: 6 5; opacity: 0.7; }
|
||||
.creature { cursor: pointer; }
|
||||
|
||||
Reference in New Issue
Block a user