Rev 23: POWER DRAIN drains the number played; the troll punches walls; the first-person stack fits

POWER DRAIN's gain is the number played, BLUNTed or ABSORBed or not
(FAQ: the counter blunts the damage done, not the drain), and a wall
drained for its points gives them up too. Older games gave only what
the opponent lost and nothing from a wall, and replay so.

"This rock-hard beast can punch a player (or a wall, etc.)": a commanded
troll now punches a wall line beside it for a D4, once a turn — a new
command, so no old game changes.

Under the first-person pane the board strip could run into the dock on
a short or tall window; the pane now takes no more height than the row
can spare and the strip shrinks beneath it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-17 00:07:34 -04:00
co-authored by Claude Fable 5.1
parent 803c51415e
commit 80dd7a7865
6 changed files with 145 additions and 11 deletions
+19 -3
View File
@@ -603,8 +603,12 @@
const attackVsWall = $derived(
selectedCard != null && cardDef(selectedCard.cardId).cardType === "attack" && !EDGE_CARDS.has(selectedCard.cardId),
);
const trollInHand = $derived(
selectedCreature != null && selectedCard == null &&
view?.creatures.some((c) => c.id === selectedCreature && c.kind === "troll" && c.controllerId === view!.you) === true,
);
const edgeSelectMode = $derived(
(selectedCard != null && EDGE_CARDS.has(selectedCard.cardId)) || attackVsWall || punchWallMode,
(selectedCard != null && EDGE_CARDS.has(selectedCard.cardId)) || attackVsWall || punchWallMode || trollInHand,
);
const cellSelectMode = $derived(
(selectedCard != null && CELL_CARDS.has(selectedCard.cardId)) || pendingCellFor !== null,
@@ -1182,6 +1186,11 @@
punchWallMode = false;
return;
}
// A commanded troll punches the wall line beside it.
if (selectedCreature && !selectedCard) {
dispatch({ type: "creatureAttackWall", creatureId: selectedCreature, cell, side });
return;
}
if (!selectedCard || !edgeSelectMode) return;
// The attached number rides along for every edge cast: wand charges,
// wall-of-fire durations, wall attacks alike.
@@ -2712,7 +2721,7 @@
<span>Your <strong>double</strong> stands ready — select a card and it casts from the double's square, by the double's sight.</span>
{:else}
<span>Commanding <strong>{cardDef(sc.kind).name}</strong> {creatureStats(sc)}.
Tap a square beside it to march, a target in its square to attack.</span>
Tap a square beside it to march, a target in its square to attack{sc.kind === "troll" ? ", or a wall line beside it to punch" : ""}.</span>
{/if}
{#if dmWarnCell}
<span class="hint-alert"> it will claw YOU the moment it enters your square tap again if you mean it</span>
@@ -4233,7 +4242,14 @@
.table-stack { flex: 1 1 0; min-height: 0; }
.board-frame { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
.board-viewport { height: auto; flex: 1 1 0; min-height: 0; }
.fpv-primary .board-viewport { flex: 0 0 30dvh; }
/* Under the first-person pane the board is a strip that may shrink
further, never overflowing into the dock beneath. */
.fpv-primary .board-viewport { flex: 0 1 30dvh; min-height: 5rem; }
.fpv-primary .board-frame { min-height: 0; }
/* The pane keeps its aspect but never more height than the row can
spare above the board strip and the dock. */
.fpv-primary :global(.live-fp) { flex: 0 1 auto; min-height: 0; }
.fpv-primary :global(.fpv-canvas) { width: auto; max-width: 100%; max-height: calc(100dvh - 34.5rem); margin: 0 auto; }
.game:not(.hand-left) .table-edge { margin-top: 0.5rem; }
.game.hand-left {
grid-template-columns: minmax(17.4rem, 24rem) minmax(340px, 1fr) minmax(250px, 330px);
+5 -2
View File
@@ -152,7 +152,9 @@ export function humanize(e: GameEvent): string | null {
case "sectorRelocated": return `The maze SHUDDERS — an entire sector slides away!`;
case "creatureCreated": return `${e.controller} summons a ${e.kind.replace(/-/g, " ")}!`;
case "creatureMoved": return null;
case "creatureAttacked": return e.dieRoll != null ? `The ${spellName(e.kind)} swings (rolled ${e.dieRoll})!` : `The ${spellName(e.kind)} strikes!`;
case "creatureAttacked":
if (e.target === "wall") return `The ${spellName(e.kind)} punches the wall (rolled ${e.dieRoll ?? "?"})!`;
return e.dieRoll != null ? `The ${spellName(e.kind)} swings (rolled ${e.dieRoll})!` : `The ${spellName(e.kind)} strikes!`;
case "creatureTouched": return `The ${spellName(e.kind)} falls upon ${e.player}!`;
case "creatureDamaged": return e.amount > 0 ? `The ${spellName(e.kind)} takes ${e.amount} damage.` : `The attack has no effect on the ${spellName(e.kind)}.`;
case "creatureDestroyed": return `The ${e.kind.replace(/-/g, " ")} is destroyed (${e.by})!`;
@@ -219,7 +221,8 @@ export function humanize(e: GameEvent): string | null {
case "slimeWashed": return `The wave washes the slime away.`;
case "wallDamaged": {
const what = e.needed === 15 ? "door" : "wall";
return `${e.player} batters the ${what} with ${e.source === "punch" ? "bare fists" : cardDef(e.source).name}${e.total}/${e.needed}.`;
const weapon = e.source === "punch" ? "bare fists" : e.source === "troll" ? "the troll's fist" : cardDef(e.source).name;
return `${e.player} batters the ${what} with ${weapon}${e.total}/${e.needed}.`;
}
case "treasureDropped": return e.onHomeOf ? `${e.player} drops a treasure on ${e.onHomeOf}'s home base!` : `${e.player} drops a treasure.`;
case "playerEliminated": return e.reason === "treasuresLost" ? `${e.player} is eliminated — both treasures lost!` : null;
+8
View File
@@ -259,6 +259,14 @@ export const HOUSE_RULINGS: HouseRuling[] = [
id: "teleport", title: "Teleporting across the maze's edge", cards: ["teleport"],
body: ["TELEPORT ignores the maze's outer edge as it ignores any wall. A teleporter leaving the maze at any square's edge re-enters at the opposite edge on the same line, one space on — the lettered openings are not needed. Four spaces straight up from two squares below the top edge lands two squares up from the bottom."],
},
{
id: "power-drain", title: "Power Drain", cards: ["power-drain", "blunt", "absorb"],
body: ["POWER DRAIN drains the number played. The caster gains it whether the blow is BLUNTed or ABSORBed — the FAQ has the counter blunting the damage done, not the drain — and a wall drained for its points gives them up as a wizard would. A FULL SHIELD stops the drain with the spell."],
},
{
id: "troll", title: "The troll's fist", cards: ["troll"],
body: ["A commanded TROLL punches a wall line beside it as it punches a wizard: a D4 of damage toward the wall's fall, once a turn."],
},
{
id: "pits", title: "Crossing a pit", cards: ["create-pit"],
body: [