diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 36c406d..f2e546f 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -270,10 +270,11 @@ export interface GameConfig { * Rev 10: BUTT-HEAD's ram is MOVEMENT — the charge walks real * corridors on the turn's legal legs (three plus numbers) and spends * them. - * Rev 11: a REVERSE against SLOW DEATH turns the whole curse (FAQ) — - * the victim gains a point for every card drawn, permanently — and a - * FULL REFLECTION returns a permanent curse (SLOW DEATH, WALKING DEAD, - * IDIOT) onto its caster instead of letting it evaporate. + * Rev 11: a REVERSE against SLOW DEATH or WALKING DEAD turns the whole + * curse (FAQ) — a point gained per card drawn, half a point per space + * walked, permanently — and a FULL REFLECTION returns a permanent + * curse (SLOW DEATH, WALKING DEAD, IDIOT) onto its caster instead of + * letting it evaporate. */ deckRev?: number; } @@ -2219,9 +2220,13 @@ const CARD_EFFECTS: Record baseDamage: () => 0, permanentCurse: true, // "1/2 point of damage for every space moved. This spell is permanent." + // Rev 11: REVERSED at cast, the walk heals instead — half a point + // gained per space, as permanently. Older games attached it biting. onResolved: (ctx) => { if (ctx.fullyStopped || !ctx.defender.alive) return; - attachSustained(ctx.state, ctx.events, "walking-dead", ctx.attacker.id, ctx.defender.id, PERMANENT_TURNS); + const reversed = ctx.reversed && (ctx.state.config.deckRev ?? 1) >= 11; + attachSustained(ctx.state, ctx.events, "walking-dead", ctx.attacker.id, ctx.defender.id, + PERMANENT_TURNS, reversed ? { reversed: 1 } : {}); }, }, disease: { @@ -4312,12 +4317,18 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult { state.turn.movementUsed++; events.push({ type: "moved", player: p.id, from, to: p.position, direction, via }); - // WALKING DEAD: 1/2 point per space moved (a full point every two steps). + // WALKING DEAD: 1/2 point per space moved (a full point every two + // steps) — gained instead of bled for a curse REVERSED at its casting. for (const fx of sustainedOn(state, p.id, "walking-dead")) { fx.data.halfSteps = (fx.data.halfSteps ?? 0) + 1; if (fx.data.halfSteps % 2 === 0) { - applyDamage(state, events, p, 1, "walking dead", null); - checkVictory(state, events); + if (fx.data.reversed) { + p.life += 1; + events.push({ type: "lifeGained", player: p.id, amount: 1, source: "walking dead (reversed)", lifeAfter: p.life }); + } else { + applyDamage(state, events, p, 1, "walking dead", null); + checkVictory(state, events); + } } } // DISEASE: the carrier infects every other player in a square they enter. diff --git a/packages/engine/test/stones.test.ts b/packages/engine/test/stones.test.ts index 0091b74..f2777fd 100644 --- a/packages/engine/test/stones.test.ts +++ b/packages/engine/test/stones.test.ts @@ -170,6 +170,32 @@ describe("slow death", () => { expect(state.players.find((p) => p.id === defender)!.life).toBe(17); }); + it("a reversed walking dead heals half a point per space walked (rev 11)", () => { + let { state } = newGame(); + state = toRound2(state); + const { attacker, defender } = faceOff(state); + const wd = giveCard(state, attacker, "walking-dead"); + giveCard(state, defender, "reverse", "RV", 0); + const r = applyCommand(state, attacker, { + type: "cast", instanceId: wd.instanceId, target: { kind: "player", playerId: defender }, + }); + if (!r.ok) throw new Error(r.error); + state = must(r.state, defender, { type: "counteract", instanceId: "reverse#RV" }); + state = drain(state); + expect(sustainedOn(state, defender, "walking-dead").length).toBe(1); + + state = must(state, attacker, { type: "endTurn", draw: 0 }); + // Two spaces walked: one point gained, not bled. + let steps = 0; + for (const dir of ["N", "S", "E", "W", "N", "S", "E", "W"] as const) { + if (steps >= 2) break; + const mv = applyCommand(state, defender, { type: "move", direction: dir }); + if (mv.ok) { state = mv.state; steps++; } + } + expect(steps).toBe(2); + expect(state.players.find((p) => p.id === defender)!.life).toBe(16); + }); + it("a full reflection returns the curse onto its caster (rev 11)", () => { let { state } = newGame(); state = toRound2(state);