Wall of Fire takes the rim warps (rules rev 39)

A warp mouth at the maze's edge is a corridor — the wraparound connects
it to the opposite rim — so the fire may bar it. It takes BOTH mouths
(each with its own burn-out clock), and the crossing steps through flame
for the usual 4 from either side. Older ledgers refused the cast and
replay so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-21 11:53:08 -04:00
co-authored by Claude Fable 5
parent 2afc4e3973
commit f82e4625af
3 changed files with 70 additions and 18 deletions
+40 -17
View File
@@ -20,6 +20,7 @@ import {
assembleBoard,
cellKey,
edgeKey,
findWarp,
hasLineOfSight,
sightBetween,
neighbor,
@@ -1410,25 +1411,40 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
if (!cmd.target || cmd.target.kind !== "edge") return "wall of fire targets a corridor edge";
const { cell, side } = cmd.target;
const view = boardView(state);
if (!view.cells[cellKey(cell)] || !view.cells[cellKey(neighbor(cell, side))]) {
// A warp mouth at the maze's rim is a corridor too — the wraparound
// connects it to the opposite edge (rev 39; older ledgers refused it
// and replay so). The fire takes BOTH mouths, so the crossing burns
// no matter which rim the wizard steps in from.
const offBoard = !view.cells[cellKey(neighbor(cell, side))];
const warp = offBoard && (state.config.deckRev ?? 1) >= 39
? findWarp(view, cell, side) : undefined;
if (!view.cells[cellKey(cell)] || (offBoard && !warp)) {
return "the fire must span a corridor between two spaces";
}
const key = edgeKey(cell, side);
if ((view.edges[key] ?? "open") !== "open") return "that corridor is not open";
const farKey = warp ? edgeKey(warp.to.cell, warp.to.side) : null;
if (farKey && (view.edges[farKey] ?? "open") !== "open") {
return "the far mouth of that warp is sealed";
}
if (!losToEdge(view, caster.position, cell, side)) return "no line of sight";
state.edgeOverrides[key] = "firewall";
state.createdEdges[key] = true;
const fx: SustainedEffect = {
id: `fx-${state.nextEffectId++}`,
cardId: "wall-of-fire",
casterId: caster.id,
targetId: caster.id,
remainingTurns: Math.max(1, magnitude.duration),
data: {},
edge: key,
const ignite = (k: string, at: { cell: Cell; side: Side }) => {
state.edgeOverrides[k] = "firewall";
state.createdEdges[k] = true;
const fx: SustainedEffect = {
id: `fx-${state.nextEffectId++}`,
cardId: "wall-of-fire",
casterId: caster.id,
targetId: caster.id,
remainingTurns: Math.max(1, magnitude.duration),
data: {},
edge: k,
};
state.sustained.push(fx);
events.push({ type: "firewallCreated", caster: caster.id, edge: at, turns: fx.remainingTurns });
};
state.sustained.push(fx);
events.push({ type: "firewallCreated", caster: caster.id, edge: { cell, side }, turns: fx.remainingTurns });
ignite(key, { cell, side });
if (warp && farKey) ignite(farKey, { cell: warp.to.cell, side: warp.to.side });
return null;
},
},
@@ -3856,10 +3872,17 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
const edge = view.edges[key] ?? "open";
const dest = neighbor(p.position, direction);
if (!view.cells[cellKey(dest)]) {
if (isBlinded(state, p)) return blindBump(state, events, p, direction);
return err("blocked");
}
if (edge === "door" && doorIsOpen(state, key)) {
// A fire on a warp mouth (rev 39 casts allow it): the wraparound
// corridor still runs — through flame.
const warp = edge === "firewall" ? findWarp(view, p.position, direction) : undefined;
if (!warp) {
if (isBlinded(state, p)) return blindBump(state, events, p, direction);
return err("blocked");
}
p.position = { ...warp.to.cell };
via = "warp";
crossedFirewall = true;
} else if (edge === "door" && doorIsOpen(state, key)) {
p.position = dest;
via = "step";
} else if (edge === "firewall") {