diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index cf42574..1a2b4ee 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -2347,8 +2347,11 @@ function waveFromEdge(state: GameState, events: GameEvent[], cell: Cell, side: S if (p.alive && cellKey(p.position) === cellKey(probe)) washBackN(state, events, p, dir, range); } for (const c of [...state.creatures]) { - if (c.kind === "fire-imp" && cellKey(c.position) === cellKey(probe)) { + if (cellKey(c.position) !== cellKey(probe)) continue; + if (c.kind === "fire-imp") { destroyCreature(state, events, c, reason); + } else if ((state.config.deckRev ?? 1) >= 17) { + washBackCreature(state, events, c, dir, range); } } if (state.squareContents[cellKey(probe)]?.kind === "slime") { @@ -2371,8 +2374,11 @@ function waveFromCell(state: GameState, events: GameEvent[], center: Cell, range if (p.alive && cellKey(p.position) === cellKey(probe)) washBackN(state, events, p, dir, range); } for (const c of [...state.creatures]) { - if (c.kind === "fire-imp" && cellKey(c.position) === cellKey(probe)) { + if (cellKey(c.position) !== cellKey(probe)) continue; + if (c.kind === "fire-imp") { destroyCreature(state, events, c, "rushing water"); + } else if ((state.config.deckRev ?? 1) >= 17) { + washBackCreature(state, events, c, dir, range); } } } @@ -2473,6 +2479,25 @@ function washBackN(state: GameState, events: GameEvent[], p: PlayerState, dir: S } } +/** The wave carries monsters as it carries wizards (rules rev 17): washed + * back, and crushed a point per space the maze refuses them. Fire imps + * never reach this — water destroys them outright. */ +function washBackCreature(state: GameState, events: GameEvent[], c: CreatureState, dir: Side, range: number): void { + const view = boardView(state); + let moved = 0; + for (let i = 0; i < range; i++) { + const step = stepTarget(view, c.position, dir); + if (step.kind === "blocked") break; + if (state.squareContents[cellKey(step.to)]?.kind === "stone") break; + c.position = step.to; + moved++; + } + const blockedSpaces = range - moved; + if (blockedSpaces > 0) { + damageCreature(state, events, c, blockedSpaces, "waterwall crush"); + } +} + // --------------------------------------------------------------------------- // Creatures diff --git a/packages/engine/test/creatures.test.ts b/packages/engine/test/creatures.test.ts index ab664b8..a5104a6 100644 --- a/packages/engine/test/creatures.test.ts +++ b/packages/engine/test/creatures.test.ts @@ -550,3 +550,42 @@ describe("creatures walk open doorways", () => { throw new Error("setup: seed 42 grew a maze with no doors"); }); }); + +describe("the wave carries monsters (rules rev 17)", () => { + it("a cornered skeleton takes the waterwall crush", () => { + let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"], deckRev: 17 }); + state = toRound2(state); + const caster = activePlayer(state); + // A skeleton pinned against solid stone, one square from the wave's edge. + const view = boardView(state); + for (const [key, edge] of Object.entries(view.edges)) { + if (edge !== "wall") continue; + const [kind, coords] = key.split(":") as [string, string]; + if (kind !== "H") continue; + const [x, y] = coords.split(",").map(Number) as [number, number]; + // Wave from this wall southward washes the square below. + const below = { x, y: y + 1 }; + if (!view.cells[cellKey(below)]) continue; + state.creatures.push({ + id: "sk1", kind: "skeleton", controllerId: caster.id, + position: { ...below }, damage: 0, maxDamage: 4, movesPerTurn: 3, + movementUsed: 0, attackUsed: false, justCreated: false, + wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [], + }); + // Pin it: stone directly south of it. + state.squareContents[cellKey({ x, y: y + 2 })] = { kind: "stone", damage: 0, createdBy: caster.id }; + caster.position = { x, y }; + const stw = giveCard(state, caster.id, "stone-to-water"); + const r = applyCommand(state, caster.id, { + type: "cast", instanceId: stw.instanceId, + target: { kind: "edge", cell: { x, y }, side: "S" }, + }); + if (!r.ok) throw new Error(r.error); + const sk = r.state.creatures.find((c) => c.id === "sk1"); + // Washed against the stone: it took crush damage (or died of it). + if (sk) expect(sk.damage).toBeGreaterThan(0); + return; + } + throw new Error("setup: no horizontal wall with a floor below"); + }); +}); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 4bbf0bc..0d51302 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -54,7 +54,7 @@ export interface Room { const rooms = new Map(); /** Rules revision new games are dealt under (stored games keep their own). */ -const RULES_REV = 16; +const RULES_REV = 17; const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; diff --git a/packages/web/src/local.svelte.ts b/packages/web/src/local.svelte.ts index 7c7c309..b76b5f6 100644 --- a/packages/web/src/local.svelte.ts +++ b/packages/web/src/local.svelte.ts @@ -136,7 +136,7 @@ class LocalGame { seed, sets: expansion ? ["basic", "expansion1"] : ["basic"], ...(colors ? { colors } : {}), - deckRev: 16, + deckRev: 17, }; const { state, events } = createGame(config); for (const e of events) {