Every wall card learns the rim (rules rev 40)
The full audit Wall of Fire started: WATERWALL crashes on a warp mouth, washing both rims inward; ILLUSION WALL hangs its scrim on a mouth (the create-wall bluff); STONE TO WATER melts a brick over a warp at both ends and breaches a plain rim wall exactly as DESTROY WALL does — far side washes out, a warp opens, the wave floods the fresh tunnel; the WARP WAND bores a temporary wraparound that closes when its wall returns; and CREATE DOOR stops hanging dead doors onto the void. Older ledgers keep their refusals and danglings, and replay so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f82e4625af
commit
e8df6efcde
@@ -289,7 +289,12 @@ export interface GameState {
|
||||
/** Remaining charges per wand card instance (set on first use). */
|
||||
wandCharges: Record<string, number>;
|
||||
/** WARP WAND: walls opened for this turn only, with their prior state. */
|
||||
tempWarpEdges: { key: string; prior: EdgeState | null }[];
|
||||
tempWarpEdges: {
|
||||
key: string;
|
||||
prior: EdgeState | null;
|
||||
/** A rim wall warped open bores a temporary wraparound; unwind it too. */
|
||||
unwarp?: { a: Cell; aSide: Side; b: Cell; bSide: Side };
|
||||
}[];
|
||||
/** BOOBYTRAP: four face-down tokens, one real (its cell key is secret). */
|
||||
boobytraps: { casterId: PlayerId; cells: Cell[]; realKey: string }[];
|
||||
/** GLUE: object cells that cannot be picked up from, by cell key. */
|
||||
@@ -1456,7 +1461,11 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
||||
if (!cmd.target || cmd.target.kind !== "edge") return "waterwall 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 rim warp mouth is a corridor too (rev 40): the collapse washes
|
||||
// both rims inward (waveFromEdge follows the tunnel).
|
||||
const offBoard = !view.cells[cellKey(neighbor(cell, side))];
|
||||
const warpMouth = offBoard && (state.config.deckRev ?? 1) >= 40 && findWarp(view, cell, side);
|
||||
if (!view.cells[cellKey(cell)] || (offBoard && !warpMouth)) {
|
||||
return "the wave must span a corridor between two spaces";
|
||||
}
|
||||
if (!losToEdge(view, caster.position, cell, side)) return "no line of sight";
|
||||
@@ -1615,7 +1624,12 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
||||
if (!cmd.target || cmd.target.kind !== "edge") return "illusion wall targets a wall edge";
|
||||
const { cell, side } = cmd.target;
|
||||
const view = boardView(state);
|
||||
if (!view.cells[cellKey(cell)] || !view.cells[cellKey(neighbor(cell, side))]) {
|
||||
// A rim warp mouth takes the scrim too (rev 40) — the bluff of
|
||||
// create-wall's brick-over. The illusion hangs on the mouth it was
|
||||
// painted on; the far mouth shows nothing (nothing physical exists).
|
||||
const offBoard = !view.cells[cellKey(neighbor(cell, side))];
|
||||
const warpMouth = offBoard && (state.config.deckRev ?? 1) >= 40 && findWarp(view, cell, side);
|
||||
if (!view.cells[cellKey(cell)] || (offBoard && !warpMouth)) {
|
||||
return "the illusion must span two spaces on the board";
|
||||
}
|
||||
const key = edgeKey(cell, side);
|
||||
@@ -1788,6 +1802,27 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
||||
state.tempWarpEdges.push({ key, prior: state.edgeOverrides[key] ?? null });
|
||||
state.edgeOverrides[key] = "open";
|
||||
events.push({ type: "wallWarpedOpen", player: caster.id, edge: { cell, side } });
|
||||
// A rim wall warped open bores a temporary wraparound (rev 40): the
|
||||
// far rim wall vanishes with it and a warp runs until turn's end.
|
||||
// (Pre-40 rim casts opened a dead edge and replay so.)
|
||||
const offBoard = !view.cells[cellKey(neighbor(cell, side))];
|
||||
if ((state.config.deckRev ?? 1) >= 40 && offBoard && !findWarp(view, cell, side)) {
|
||||
const far = oppositePerimeter(view, cell, side);
|
||||
const farKey = far ? edgeKey(far.cell, far.side) : "";
|
||||
if (far && (view.edges[farKey] ?? "open") === "wall") {
|
||||
state.tempWarpEdges.push({
|
||||
key: farKey, prior: state.edgeOverrides[farKey] ?? null,
|
||||
unwarp: { a: cell, aSide: side, b: far.cell, bSide: far.side },
|
||||
});
|
||||
state.edgeOverrides[farKey] = "open";
|
||||
events.push({ type: "wallWarpedOpen", player: caster.id, edge: far });
|
||||
state.board.warps.push(
|
||||
{ from: { cell, side }, to: { cell: far.cell, side: far.side } },
|
||||
{ from: { cell: far.cell, side: far.side }, to: { cell, side } },
|
||||
);
|
||||
events.push({ type: "warpOpened", a: { cell, side }, b: far });
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
@@ -1841,6 +1876,11 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
||||
const { cell, side } = cmd.target;
|
||||
const key = edgeKey(cell, side);
|
||||
const view = boardView(state);
|
||||
// No doors through the maze's rim (rev 40): a rim door would open onto
|
||||
// nothing traversable — the wraparound belongs to breaches, not doors.
|
||||
if ((state.config.deckRev ?? 1) >= 40 && !view.cells[cellKey(neighbor(cell, side))]) {
|
||||
return "the door must stand between two spaces";
|
||||
}
|
||||
if ((view.edges[key] ?? "open") === "open") {
|
||||
if (!view.cells[cellKey(cell)] || !view.cells[cellKey(neighbor(cell, side))]) {
|
||||
return "the door must stand between two spaces";
|
||||
@@ -1964,6 +2004,35 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
||||
delete state.doorStates[key];
|
||||
delete state.createdEdges[key];
|
||||
events.push({ type: "stoneTurnedToWater", caster: caster.id, at: null, edge: { cell, side } });
|
||||
if ((state.config.deckRev ?? 1) >= 40) {
|
||||
// A brick over a warp mouth melts at BOTH ends — the tunnel is one
|
||||
// wall line (rev 40, mirroring DESTROY WALL's pair rule).
|
||||
const pair = pairedWarpMouth(view, cell, side);
|
||||
const pairKey = pair ? edgeKey(pair.cell, pair.side) : "";
|
||||
if (pair && state.createdEdges[pairKey]) {
|
||||
state.edgeOverrides[pairKey] = "open";
|
||||
delete state.createdEdges[pairKey];
|
||||
events.push({ type: "stoneTurnedToWater", caster: caster.id, at: null, edge: pair });
|
||||
}
|
||||
// Melting the maze's outer rim breaches it like DESTROY WALL does:
|
||||
// the far rim wall washes out too and a new warp opens, so the
|
||||
// wave below pours through the fresh tunnel onto both rims.
|
||||
const offBoard = !view.cells[cellKey(neighbor(cell, side))];
|
||||
if (offBoard && !pair) {
|
||||
const far = oppositePerimeter(view, cell, side);
|
||||
const farKey = far ? edgeKey(far.cell, far.side) : "";
|
||||
if (far && (boardView(state).edges[farKey] ?? "open") === "wall") {
|
||||
state.edgeOverrides[farKey] = "open";
|
||||
delete state.createdEdges[farKey];
|
||||
events.push({ type: "stoneTurnedToWater", caster: caster.id, at: null, edge: far });
|
||||
state.board.warps.push(
|
||||
{ from: { cell, side }, to: { cell: far.cell, side: far.side } },
|
||||
{ from: { cell: far.cell, side: far.side }, to: { cell, side } },
|
||||
);
|
||||
events.push({ type: "warpOpened", a: { cell, side }, b: far });
|
||||
}
|
||||
}
|
||||
}
|
||||
// "Wall turns into a WATERWALL with a range and damage of 2."
|
||||
waveFromEdge(state, events, cell, side, 2);
|
||||
checkVictory(state, events);
|
||||
@@ -2608,10 +2677,21 @@ function waveForce(state: GameState, range: number, dist: number): number {
|
||||
|
||||
/** A collapsing waterwall wave from an edge: wash players back `range`. */
|
||||
function waveFromEdge(state: GameState, events: GameEvent[], cell: Cell, side: Side, range: number, reason = "rushing water"): void {
|
||||
const pushes: { start: Cell; dir: Side }[] = [
|
||||
{ start: cell, dir: opposite(side) },
|
||||
{ start: neighbor(cell, side), dir: side },
|
||||
];
|
||||
// A warp mouth's "far side" is the opposite rim: the collapse washes both
|
||||
// mouths inward (rev 40; older ledgers' rim waves pushed into the void
|
||||
// and replay so). A plain edge washes its two adjacent cells apart.
|
||||
const warp = (state.config.deckRev ?? 1) >= 40 &&
|
||||
!boardView(state).cells[cellKey(neighbor(cell, side))]
|
||||
? findWarp(boardView(state), cell, side) : undefined;
|
||||
const pushes: { start: Cell; dir: Side }[] = warp
|
||||
? [
|
||||
{ start: cell, dir: opposite(side) },
|
||||
{ start: warp.to.cell, dir: opposite(warp.to.side) },
|
||||
]
|
||||
: [
|
||||
{ start: cell, dir: opposite(side) },
|
||||
{ start: neighbor(cell, side), dir: side },
|
||||
];
|
||||
for (const { start, dir } of pushes) {
|
||||
let probe = start;
|
||||
for (let dist = 0; dist < range; dist++) {
|
||||
@@ -6308,6 +6388,12 @@ function doEndTurn(prev: GameState, draw: number): CommandResult {
|
||||
for (const t of state.tempWarpEdges) {
|
||||
if (t.prior === null) delete state.edgeOverrides[t.key];
|
||||
else state.edgeOverrides[t.key] = t.prior;
|
||||
if (t.unwarp) {
|
||||
const { a, aSide, b, bSide } = t.unwarp;
|
||||
state.board.warps = state.board.warps.filter((w) =>
|
||||
!(cellKey(w.from.cell) === cellKey(a) && w.from.side === aSide) &&
|
||||
!(cellKey(w.from.cell) === cellKey(b) && w.from.side === bSide));
|
||||
}
|
||||
}
|
||||
events.push({ type: "wallsWarpedBack", count: state.tempWarpEdges.length });
|
||||
state.tempWarpEdges = [];
|
||||
|
||||
Reference in New Issue
Block a user