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:
co-authored by
Claude Fable 5.1
parent
803c51415e
commit
80dd7a7865
@@ -247,7 +247,7 @@ export interface CastParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
||||||
export const CURRENT_RULES_REV = 22;
|
export const CURRENT_RULES_REV = 23;
|
||||||
|
|
||||||
/** Every rulings revision since the baseline, newest last — the entries a
|
/** Every rulings revision since the baseline, newest last — the entries a
|
||||||
* game's deckRev freezes it before or after. Shown to players as the house
|
* game's deckRev freezes it before or after. Shown to players as the house
|
||||||
@@ -274,6 +274,7 @@ export const RULES_REVISIONS: { rev: number; note: string }[] = [
|
|||||||
{ rev: 20, note: "A waterwall's wave names its victims before it pushes any of them. Before, a wave walking the way it pushed could catch a wizard it had just shoved and shove them again with the force left — one square into a wall cost two points instead of one." },
|
{ rev: 20, note: "A waterwall's wave names its victims before it pushes any of them. Before, a wave walking the way it pushed could catch a wizard it had just shoved and shove them again with the force left — one square into a wall cost two points instead of one." },
|
||||||
{ rev: 21, note: "Two cards keep their whole promise. LIFESAVER's holder is not eliminated for losing both treasures. FORCE FIELD, after stopping the spell, stands until the end of the opponent's turn: they may not enter its caster's square, nor cast on or past them — on every side, where the card says one." },
|
{ rev: 21, note: "Two cards keep their whole promise. LIFESAVER's holder is not eliminated for losing both treasures. FORCE FIELD, after stopping the spell, stands until the end of the opponent's turn: they may not enter its caster's square, nor cast on or past them — on every side, where the card says one." },
|
||||||
{ rev: 22, note: "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. Older games crossed the edge only at the lettered openings." },
|
{ rev: 22, note: "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. Older games crossed the edge only at the lettered openings." },
|
||||||
|
{ rev: 23, note: "POWER DRAIN drains the number played: the caster gains it whether the blow is BLUNTed or ABSORBed (FAQ: the counter blunts the damage done, not the drain), and a wall drained for its points gives them up too. Older games gave the caster only what the opponent lost, and nothing from a wall." },
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface GameConfig {
|
export interface GameConfig {
|
||||||
@@ -845,6 +846,7 @@ export type Command =
|
|||||||
| { type: "moveCreature"; creatureId: string; direction: Side }
|
| { type: "moveCreature"; creatureId: string; direction: Side }
|
||||||
| { type: "creatureWarpStep"; creatureId: string }
|
| { type: "creatureWarpStep"; creatureId: string }
|
||||||
| { type: "creatureAttack"; creatureId: string; targetId: string }
|
| { type: "creatureAttack"; creatureId: string; targetId: string }
|
||||||
|
| { type: "creatureAttackWall"; creatureId: string; cell: Cell; side: Side }
|
||||||
| {
|
| {
|
||||||
type: "cast";
|
type: "cast";
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
@@ -1124,12 +1126,21 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
|||||||
requiresLos: true,
|
requiresLos: true,
|
||||||
baseDamage: (n) => n ?? 1,
|
baseDamage: (n) => n ?? 1,
|
||||||
onResolved: (ctx) => {
|
onResolved: (ctx) => {
|
||||||
if (ctx.damageDealt <= 0 || !ctx.attacker.alive) return;
|
if (!ctx.attacker.alive) return;
|
||||||
ctx.attacker.life += ctx.damageDealt;
|
// Rev 23: the drain is the number played, counters or no — "he is
|
||||||
|
// blunting damage done, not acting upon the attack spell itself"
|
||||||
|
// (FAQ). A returned drain carries its settled amount. Older games
|
||||||
|
// drained only what the opponent lost.
|
||||||
|
const modern = (ctx.state.config.deckRev ?? 1) >= 23 && !ctx.stack.reflectedBase;
|
||||||
|
const gain = modern
|
||||||
|
? (ctx.fullyStopped || ctx.reversed ? 0 : (ctx.stack.numberValue ?? 1) * ctx.stack.amplifyFactor)
|
||||||
|
: ctx.damageDealt;
|
||||||
|
if (gain <= 0) return;
|
||||||
|
ctx.attacker.life += gain;
|
||||||
ctx.events.push({
|
ctx.events.push({
|
||||||
type: "lifeGained",
|
type: "lifeGained",
|
||||||
player: ctx.attacker.id,
|
player: ctx.attacker.id,
|
||||||
amount: ctx.damageDealt,
|
amount: gain,
|
||||||
source: "power drain",
|
source: "power drain",
|
||||||
lifeAfter: ctx.attacker.life,
|
lifeAfter: ctx.attacker.life,
|
||||||
});
|
});
|
||||||
@@ -3428,6 +3439,31 @@ function doCreatureAttack(prev: GameState, creatureId: string, targetId: string)
|
|||||||
return { ok: true, state, events };
|
return { ok: true, state, events };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** "This rock-hard beast can punch a player (or a wall, etc.)": the troll's
|
||||||
|
* fist on a wall line beside it, a D4 of damage toward the wall's fall. */
|
||||||
|
function doCreatureAttackWall(prev: GameState, creatureId: string, cell: Cell, side: Side): CommandResult {
|
||||||
|
const blocked = requireActionsAvailable(prev);
|
||||||
|
if (blocked) return err(blocked);
|
||||||
|
if (prev.turn.round === 1) return err("no combat during the first round of turns");
|
||||||
|
const state = clone(prev);
|
||||||
|
const active = activePlayer(state);
|
||||||
|
const creature = creatureById(state, creatureId);
|
||||||
|
if (!creature) return err("no such creature");
|
||||||
|
if (creature.controllerId !== active.id) return err("that creature does not obey you");
|
||||||
|
if (creature.kind !== "troll") return err("only the troll punches walls");
|
||||||
|
if (creature.justCreated) return err("it cannot attack the turn it was created");
|
||||||
|
if (creature.attackUsed) return err("that creature has already attacked this turn");
|
||||||
|
if (!touchesEdge(creature.position, cell, side)) return err("the troll must stand beside the wall");
|
||||||
|
const events: GameEvent[] = [];
|
||||||
|
const [roll, rngNext] = rollDie(state.rng);
|
||||||
|
state.rng = rngNext;
|
||||||
|
creature.attackUsed = true;
|
||||||
|
events.push({ type: "creatureAttacked", creatureId: creature.id, kind: creature.kind, target: "wall", dieRoll: roll });
|
||||||
|
const problem = damageWall(state, events, active, cell, side, roll, "troll");
|
||||||
|
if (problem) return err(problem);
|
||||||
|
return { ok: true, state, events };
|
||||||
|
}
|
||||||
|
|
||||||
/** UGLY: breadth-first flee to the nearest cell out of the horror's sight. */
|
/** UGLY: breadth-first flee to the nearest cell out of the horror's sight. */
|
||||||
function retreatFromSight(state: GameState, events: GameEvent[], p: PlayerState, horror: Cell): void {
|
function retreatFromSight(state: GameState, events: GameEvent[], p: PlayerState, horror: Cell): void {
|
||||||
const view = boardView(state);
|
const view = boardView(state);
|
||||||
@@ -4131,6 +4167,7 @@ function applyCommandInner(state: GameState, playerId: PlayerId, command: Comman
|
|||||||
case "moveCreature": return doMoveCreature(state, command.creatureId, command.direction);
|
case "moveCreature": return doMoveCreature(state, command.creatureId, command.direction);
|
||||||
case "creatureWarpStep": return doCreatureWarpStep(state, command.creatureId);
|
case "creatureWarpStep": return doCreatureWarpStep(state, command.creatureId);
|
||||||
case "creatureAttack": return doCreatureAttack(state, command.creatureId, command.targetId);
|
case "creatureAttack": return doCreatureAttack(state, command.creatureId, command.targetId);
|
||||||
|
case "creatureAttackWall": return doCreatureAttackWall(state, command.creatureId, command.cell, command.side);
|
||||||
case "cast": return doCast(state, command);
|
case "cast": return doCast(state, command);
|
||||||
case "setAmbush": return doSetAmbush(state, command);
|
case "setAmbush": return doSetAmbush(state, command);
|
||||||
case "cancelAmbush": return doCancelAmbush(state, command.ambushId);
|
case "cancelAmbush": return doCancelAmbush(state, command.ambushId);
|
||||||
@@ -5620,6 +5657,11 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
|
|||||||
}];
|
}];
|
||||||
const problem = damageWall(state, events2, caster, cell, side, dmg, inHand.cardId);
|
const problem = damageWall(state, events2, caster, cell, side, dmg, inHand.cardId);
|
||||||
if (problem) return err(problem);
|
if (problem) return err(problem);
|
||||||
|
// Rev 23: a wall drained for its points gives them up like anyone.
|
||||||
|
if (inHand.cardId === "power-drain" && (state.config.deckRev ?? 1) >= 23 && caster.alive) {
|
||||||
|
caster.life += dmg;
|
||||||
|
events2.push({ type: "lifeGained", player: caster.id, amount: dmg, source: "power drain", lifeAfter: caster.life });
|
||||||
|
}
|
||||||
// Thrown weapons clatter to the floor at the foot of the wall.
|
// Thrown weapons clatter to the floor at the foot of the wall.
|
||||||
if (inHand.cardId === "dagger" || inHand.cardId === "large-rock") {
|
if (inHand.cardId === "dagger" || inHand.cardId === "large-rock") {
|
||||||
const di = state.discard.findIndex((c) => c.instanceId === inHand.instanceId);
|
const di = state.discard.findIndex((c) => c.instanceId === inHand.instanceId);
|
||||||
|
|||||||
@@ -998,3 +998,27 @@ describe("the alter ego casts from its own square", () => {
|
|||||||
expect(done.players.find((p) => p.id === defender)!.life).toBe(10);
|
expect(done.players.find((p) => p.id === defender)!.life).toBe(10);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("the troll's fist on a wall", () => {
|
||||||
|
it("punches a wall line beside it for a D4, once a turn", () => {
|
||||||
|
let { state } = newGame();
|
||||||
|
state = toRound2(state);
|
||||||
|
const me = activePlayer(state).id;
|
||||||
|
const r = summon(state, me, "troll");
|
||||||
|
state = r.state;
|
||||||
|
const other = state.players.find((p) => p.id !== me)!.id;
|
||||||
|
state = must(state, me, { type: "endTurn", draw: 0 });
|
||||||
|
state = must(state, other, { type: "endTurn", draw: 0 });
|
||||||
|
const troll = state.creatures[0]!;
|
||||||
|
const board = boardView(state);
|
||||||
|
const side = SIDES.find((s) => board.edges[edgeKey(troll.position, s)] === "wall")!;
|
||||||
|
const res = applyCommand(state, me, { type: "creatureAttackWall", creatureId: troll.id, cell: troll.position, side });
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const hit = res.events.find((e) => e.type === "wallDamaged");
|
||||||
|
expect(hit && hit.type === "wallDamaged" ? hit.amount : 0).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(res.state.creatures[0]!.attackUsed).toBe(true);
|
||||||
|
const again = applyCommand(res.state, me, { type: "creatureAttackWall", creatureId: troll.id, cell: troll.position, side });
|
||||||
|
expect(again.ok).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { applyCommand, activePlayer, boardView, createGame, sustainedOn, type GameState } from "../src/game";
|
import { applyCommand, activePlayer, boardView, createGame, sustainedOn, type GameState } from "../src/game";
|
||||||
import { cellKey, edgeKey, neighbor, opposite, type Side } from "../src/board";
|
import { cellKey, edgeKey, neighbor, opposite, SIDES, type Side } from "../src/board";
|
||||||
import type { CardInstance } from "../src/cards";
|
import type { CardInstance } from "../src/cards";
|
||||||
import { newGame, must, giveCard, toRound2, faceOff, castAt } from "./helpers";
|
import { newGame, must, giveCard, toRound2, faceOff, castAt, drain } from "./helpers";
|
||||||
|
|
||||||
describe("duration spells", () => {
|
describe("duration spells", () => {
|
||||||
it("slow reduces movement to 1, blocks number cards, and halves attacks", () => {
|
it("slow reduces movement to 1, blocks number cards, and halves attacks", () => {
|
||||||
@@ -567,3 +567,44 @@ describe("the displayed MASTER KEY", () => {
|
|||||||
expect(r.ok).toBe(false);
|
expect(r.ok).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("POWER DRAIN drains the number played (rev 23)", () => {
|
||||||
|
it("gains the number even when the blow is blunted", () => {
|
||||||
|
let { state } = newGame();
|
||||||
|
state = toRound2(state);
|
||||||
|
const { attacker, defender } = faceOff(state);
|
||||||
|
const pd = giveCard(state, attacker, "power-drain");
|
||||||
|
giveCard(state, attacker, "number-4", "N", 1);
|
||||||
|
const d = state.players.find((p) => p.id === defender)!;
|
||||||
|
d.hand[0] = { instanceId: "blunt#T", cardId: "blunt" };
|
||||||
|
state = must(state, attacker, { type: "cast", instanceId: pd.instanceId, target: { kind: "player", playerId: defender }, numberInstanceIds: ["number-4#N"] });
|
||||||
|
state = must(state, defender, { type: "counteract", instanceId: "blunt#T" });
|
||||||
|
state = drain(state);
|
||||||
|
expect(state.players.find((p) => p.id === defender)!.life).toBe(13);
|
||||||
|
expect(state.players.find((p) => p.id === attacker)!.life).toBe(19);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drains a wall for its points", () => {
|
||||||
|
let { state } = newGame();
|
||||||
|
state = toRound2(state);
|
||||||
|
const me = activePlayer(state);
|
||||||
|
const board = boardView(state);
|
||||||
|
const side = SIDES.find((s) => board.edges[edgeKey(me.position, s)] === "wall")!;
|
||||||
|
const pd = giveCard(state, me.id, "power-drain");
|
||||||
|
giveCard(state, me.id, "number-3", "N", 1);
|
||||||
|
state = must(state, me.id, { type: "cast", instanceId: pd.instanceId, target: { kind: "edge", cell: me.position, side }, numberInstanceIds: ["number-3#N"] });
|
||||||
|
expect(state.wallDamage[edgeKey(me.position, side)]).toBe(3);
|
||||||
|
expect(state.players.find((p) => p.id === me.id)!.life).toBe(18);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("older games gave only what the opponent lost, and nothing from a wall", () => {
|
||||||
|
let state = toRound2(createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 22 }).state);
|
||||||
|
const me = activePlayer(state);
|
||||||
|
const board = boardView(state);
|
||||||
|
const side = SIDES.find((s) => board.edges[edgeKey(me.position, s)] === "wall")!;
|
||||||
|
const pd = giveCard(state, me.id, "power-drain");
|
||||||
|
giveCard(state, me.id, "number-3", "N", 1);
|
||||||
|
state = must(state, me.id, { type: "cast", instanceId: pd.instanceId, target: { kind: "edge", cell: me.position, side }, numberInstanceIds: ["number-3#N"] });
|
||||||
|
expect(state.players.find((p) => p.id === me.id)!.life).toBe(15);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -603,8 +603,12 @@
|
|||||||
const attackVsWall = $derived(
|
const attackVsWall = $derived(
|
||||||
selectedCard != null && cardDef(selectedCard.cardId).cardType === "attack" && !EDGE_CARDS.has(selectedCard.cardId),
|
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(
|
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(
|
const cellSelectMode = $derived(
|
||||||
(selectedCard != null && CELL_CARDS.has(selectedCard.cardId)) || pendingCellFor !== null,
|
(selectedCard != null && CELL_CARDS.has(selectedCard.cardId)) || pendingCellFor !== null,
|
||||||
@@ -1182,6 +1186,11 @@
|
|||||||
punchWallMode = false;
|
punchWallMode = false;
|
||||||
return;
|
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;
|
if (!selectedCard || !edgeSelectMode) return;
|
||||||
// The attached number rides along for every edge cast: wand charges,
|
// The attached number rides along for every edge cast: wand charges,
|
||||||
// wall-of-fire durations, wall attacks alike.
|
// 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>
|
<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}
|
{:else}
|
||||||
<span>Commanding <strong>{cardDef(sc.kind).name}</strong> — {creatureStats(sc)}.
|
<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}
|
||||||
{#if dmWarnCell}
|
{#if dmWarnCell}
|
||||||
<span class="hint-alert">⚠ it will claw YOU the moment it enters your square — tap again if you mean it</span>
|
<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; }
|
.table-stack { flex: 1 1 0; min-height: 0; }
|
||||||
.board-frame { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
|
.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; }
|
.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:not(.hand-left) .table-edge { margin-top: 0.5rem; }
|
||||||
.game.hand-left {
|
.game.hand-left {
|
||||||
grid-template-columns: minmax(17.4rem, 24rem) minmax(340px, 1fr) minmax(250px, 330px);
|
grid-template-columns: minmax(17.4rem, 24rem) minmax(340px, 1fr) minmax(250px, 330px);
|
||||||
|
|||||||
@@ -152,7 +152,9 @@ export function humanize(e: GameEvent): string | null {
|
|||||||
case "sectorRelocated": return `The maze SHUDDERS — an entire sector slides away!`;
|
case "sectorRelocated": return `The maze SHUDDERS — an entire sector slides away!`;
|
||||||
case "creatureCreated": return `${e.controller} summons a ${e.kind.replace(/-/g, " ")}!`;
|
case "creatureCreated": return `${e.controller} summons a ${e.kind.replace(/-/g, " ")}!`;
|
||||||
case "creatureMoved": return null;
|
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 "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 "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})!`;
|
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 "slimeWashed": return `The wave washes the slime away.`;
|
||||||
case "wallDamaged": {
|
case "wallDamaged": {
|
||||||
const what = e.needed === 15 ? "door" : "wall";
|
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 "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;
|
case "playerEliminated": return e.reason === "treasuresLost" ? `${e.player} is eliminated — both treasures lost!` : null;
|
||||||
|
|||||||
@@ -259,6 +259,14 @@ export const HOUSE_RULINGS: HouseRuling[] = [
|
|||||||
id: "teleport", title: "Teleporting across the maze's edge", cards: ["teleport"],
|
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."],
|
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"],
|
id: "pits", title: "Crossing a pit", cards: ["create-pit"],
|
||||||
body: [
|
body: [
|
||||||
|
|||||||
Reference in New Issue
Block a user