Card wave 4: all eight magic stones and Slow Death

Stones are displayed permanents whose powers weave into the core
systems: BLOODSTONE (-1 to every hit taken, cancels Slow Death's
drip), SOULSTONE (last three points immune to spell damage — punches
and daggers still finish the job), BRAINSTONE (+2 cards now, hand
limit 9 — limits are now per-player and dynamic), POWERSTONE (+1 per
number card played, movement included), SPEEDSTONE (+1 movement,
overridden by SLOW), SHADOWSTONE (physical damage you deal feeds your
life), SHIELDSTONE (number cards become counteractions that shave
their value off damage AND duration), VISIONSTONE (LOS through any
single wall or door, at the holder's option). Damage now carries a
spell/physical kind everywhere. SLOW DEATH is a permanent curse: one
magical point per card drawn, forever — Fireball still burns stones
off their display. Re-displaying a stone is refused (no Brainstone
double-draws). 49 basic-set cards implemented, 15 remain (the hard
five: bent LOS, Blind, Illusion Wall, Ugly, sector manipulation).
72 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-15 20:17:29 -04:00
co-authored by Claude Fable 5
parent 2d88b4ab40
commit 4ab44cc535
3 changed files with 381 additions and 18 deletions
+15 -3
View File
@@ -97,9 +97,14 @@
clearSelection();
return;
}
// Instant untargeted spells cast immediately; duration self-spells wait
// so a number card can be attached (cast via the button in the hint bar).
if (card.cardId === "speed" || card.cardId === "pass-through-wall" || card.cardId === "reuse-spell") {
// Instant untargeted spells (and stone displays) cast immediately;
// duration self-spells wait so a number card can be attached.
const INSTANT = new Set([
"speed", "pass-through-wall", "reuse-spell",
"bloodstone", "brainstone", "powerstone", "shadowstone",
"shieldstone", "soulstone", "speedstone", "visionstone",
]);
if (INSTANT.has(card.cardId)) {
net.command({ type: "cast", instanceId: card.instanceId });
clearSelection();
}
@@ -365,6 +370,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>
{/if}
</button>
{/each}
</div>
@@ -453,6 +461,10 @@
.card.marked { border-color: #c33; background: #fce6e6; }
.card-type { font-size: 0.65em; text-transform: uppercase; color: #776; }
.card-name { font-weight: 600; font-size: 0.9em; }
.displayed-badge {
font-size: 0.6em; text-transform: uppercase; color: #fff;
background: #7a5f2a; border-radius: 3px; padding: 0 4px; margin-top: 2px;
}
.actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
button { padding: 0.4rem 0.7rem; border-radius: 6px; border: 1px solid #998; background: #fff; cursor: pointer; }