Expansion wave 2: the magic wand system, plus Deja-Vu

Wands charge on first use from the number card(s) played (Amplify and
Add both work when setting charges), spend one charge per use with a
hard once-per-turn limit, stay displayed while charged, and crumble
to the discard when the last charge goes. They are isolated from
their wielder — usable under NO SPELL — and Absorb Spell cannot eat
them. BLASTER WAND fires 3-point bolts through the normal
counteraction stack; STICKY WAND webs its victim (movement -3 for a
turn, +2 to any fire damage, including hotter firewall crossings);
SHIFT WAND shoves a wizard one space in any direction straight
through stone walls; WARP WAND opens a wall section that snaps back
at the end of the turn. DEJA-VU retrieves any discard except a wand.
The wands' 5e-era "stick" subtype is renamed "wand" per Eric (the 6e
faces say Wand; Exp2 shelf cards keep their period names). Client:
charge badges, shove targeting, retrieve-by-name. 101 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-15 22:45:51 -04:00
co-authored by Claude Fable 5
parent df0675c735
commit 28949bdb7d
5 changed files with 410 additions and 15 deletions
+17 -4
View File
@@ -43,6 +43,7 @@
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",
]);
const CELL_CARDS = new Set([
"teleport", "fill-square-with-stone", "thornbush", "dispel-creation", "drag",
@@ -54,7 +55,7 @@
const SELF_CARDS = new Set([
"invisible", "shrink", "mist-body",
]);
const NAMED_CARDS = new Set(["card-erasure", "drop-object"]);
const NAMED_CARDS = new Set(["card-erasure", "drop-object", "deja-vu"]);
const edgeSelectMode = $derived(selectedCard != null && EDGE_CARDS.has(selectedCard.cardId));
const cellSelectMode = $derived(
(selectedCard != null && CELL_CARDS.has(selectedCard.cardId)) || pendingCellFor !== null,
@@ -284,7 +285,7 @@
}
return;
}
if (selectedCard.cardId === "teleport-opponent") {
if (selectedCard.cardId === "teleport-opponent" || selectedCard.cardId === "shift-wand") {
pendingCellFor = playerId;
return; // next: click the destination cell
}
@@ -437,7 +438,17 @@
{/if}
{#if selectedCard && NAMED_CARDS.has(selectedCard.cardId)}
<label>card name <input bind:value={nameInput} placeholder="e.g. Fireball or treasure" /></label>
— then click the target wizard
{#if selectedCard.cardId === "deja-vu"}
<button onclick={() => {
const id = nameToCardId(nameInput);
if (id && selectedCard) {
net.command({ type: "cast", instanceId: selectedCard.instanceId, params: { cardId: id } });
clearSelection();
}
}}>Retrieve</button>
{:else}
— then click the target wizard
{/if}
{/if}
{#if attachedMods.length > 0}
[+ {attachedMods.map((m) => cardDef(m.cardId).name).join(", ")}]
@@ -486,7 +497,9 @@
<span class="card-type">{def.cardType}</span>
<span class="card-name">{def.name}</span>
{#if view.players.find((p) => p.id === view.you)?.displayed.some((c) => c.instanceId === card.instanceId)}
<span class="displayed-badge">displayed</span>
<span class="displayed-badge">
displayed{view.wandCharges[card.instanceId] != null ? ` · ${view.wandCharges[card.instanceId]} ` : ""}
</span>
{/if}
</button>
{/each}