Rev 25: a bush or the ooze is attacked like a wall — POWER ATTACK counts, thrown weapons land at its foot
Older games took neither; both sides pinned. 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
66b5c06f17
commit
5193b12b8e
@@ -247,7 +247,7 @@ export interface CastParams {
|
||||
}
|
||||
|
||||
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
||||
export const CURRENT_RULES_REV = 24;
|
||||
export const CURRENT_RULES_REV = 25;
|
||||
|
||||
/** 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
|
||||
@@ -276,6 +276,7 @@ export const RULES_REVISIONS: { rev: number; note: string }[] = [
|
||||
{ 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." },
|
||||
{ rev: 24, note: "A REFLECTION against a permanent curse afflicts both wizards: WALKING DEAD and IDIOT say so on the card, and SLOW DEATH follows REFLECTION's own rule that a spell works for both parties. Older games left the caster untouched." },
|
||||
{ rev: 25, note: "An attack on a bush or the ooze is an attack on an inanimate object like any wall: POWER ATTACK's bought points count, and a thrown DAGGER or LARGE ROCK lands at the foot of the bush instead of vanishing into the discard. Older games took neither." },
|
||||
];
|
||||
|
||||
export interface GameConfig {
|
||||
@@ -5551,13 +5552,22 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
|
||||
if (content.kind === "ooze" && inHand.cardId !== "fireball") {
|
||||
return err("only fire hurts the ooze");
|
||||
}
|
||||
const dmg = effect.baseDamage(mods.magnitude.numberValue, cmd.params ?? null) * (2 ** mods.amplifies.length);
|
||||
// Rev 25: an inanimate object like any wall — POWER ATTACK's bought
|
||||
// points count, and thrown weapons land at its foot.
|
||||
const likeAWall = (state.config.deckRev ?? 1) >= 25;
|
||||
const dmg =
|
||||
effect.baseDamage(mods.magnitude.numberValue, cmd.params ?? null) * (2 ** mods.amplifies.length) +
|
||||
(likeAWall ? mods.powerAttackPoints : 0);
|
||||
if (dmg <= 0) return err(`that spell would not singe the ${what}`);
|
||||
const wandEvents: GameEvent[] = [];
|
||||
{
|
||||
const werr = spendWandCharge(state, caster, wandEvents);
|
||||
if (werr) return err(werr);
|
||||
}
|
||||
if (likeAWall && mods.powerAttackPoints > 0) {
|
||||
caster.life -= mods.powerAttackPoints;
|
||||
wandEvents.push({ type: "lifeTraded", player: caster.id, points: mods.powerAttackPoints, newAllowance: state.turn.movementAllowance });
|
||||
}
|
||||
consumeCast(state, caster, inHand, mods, effect.keepInHand ?? false);
|
||||
if (state.turn.attackUsed) state.turn.secondAttackUsed = true;
|
||||
state.turn.attackUsed = true;
|
||||
@@ -5573,6 +5583,14 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
|
||||
delete state.squareContents[cellKey(cell)];
|
||||
events.push({ type: "squareContentDestroyed", attacker: caster.id, cell: { ...cell }, kind: content.kind });
|
||||
}
|
||||
if (likeAWall && (inHand.cardId === "dagger" || inHand.cardId === "large-rock")) {
|
||||
const di = state.discard.findIndex((c) => c.instanceId === inHand.instanceId);
|
||||
if (di !== -1) {
|
||||
const [card] = state.discard.splice(di, 1);
|
||||
state.groundObjects[cellKey(cell)] = [...(state.groundObjects[cellKey(cell)] ?? []), card!];
|
||||
events.push({ type: "objectThrown", attacker: caster.id, cardId: inHand.cardId, landedAt: cell });
|
||||
}
|
||||
}
|
||||
return { ok: true, state, events };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,11 +727,42 @@ describe("attacks aimed at a bush or the ooze", () => {
|
||||
expect(r.state.turn.attackUsed).toBe(true);
|
||||
});
|
||||
|
||||
it("a thrown dagger scratches a rosebush, and the scratch stays", () => {
|
||||
it("a thrown dagger scratches a rosebush, the scratch stays, and the dagger lies at its foot", () => {
|
||||
const { state, me, cell } = withContent("rosebush");
|
||||
const dagger = giveCard(state, me.id, "dagger");
|
||||
const next = must(state, me.id, { type: "cast", instanceId: dagger.instanceId, target: { kind: "cell", cell } });
|
||||
expect(next.squareContents[cellKey(cell)]).toEqual(expect.objectContaining({ kind: "rosebush", damage: 3 }));
|
||||
expect(next.groundObjects[cellKey(cell)]).toEqual([expect.objectContaining({ cardId: "dagger" })]);
|
||||
expect(next.discard.some((c) => c.instanceId === dagger.instanceId)).toBe(false);
|
||||
});
|
||||
|
||||
it("POWER ATTACK's bought points count against a bush, at the caster's cost", () => {
|
||||
const { state, me, cell } = withContent("thornbush");
|
||||
const rock = giveCard(state, me.id, "large-rock");
|
||||
giveCard(state, me.id, "power-attack", "PA", 1);
|
||||
const next = must(state, me.id, {
|
||||
type: "cast", instanceId: rock.instanceId, target: { kind: "cell", cell },
|
||||
powerAttackInstanceId: "power-attack#PA", powerAttackPoints: 2,
|
||||
});
|
||||
expect(next.squareContents[cellKey(cell)]).toEqual(expect.objectContaining({ kind: "thornbush", damage: 4 }));
|
||||
expect(next.players.find((p) => p.id === me.id)!.life).toBe(13);
|
||||
});
|
||||
|
||||
it("older games let a thrown weapon vanish and POWER ATTACK count for nothing against a bush", () => {
|
||||
const state = toRound2(createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 24 }).state);
|
||||
const me = activePlayer(state);
|
||||
const { cell } = emptyNeighborCell(state, me.position);
|
||||
state.squareContents[cellKey(cell)] = { kind: "rosebush", damage: 0, createdBy: "bob" };
|
||||
const rock = giveCard(state, me.id, "large-rock");
|
||||
giveCard(state, me.id, "power-attack", "PA", 1);
|
||||
const next = must(state, me.id, {
|
||||
type: "cast", instanceId: rock.instanceId, target: { kind: "cell", cell },
|
||||
powerAttackInstanceId: "power-attack#PA", powerAttackPoints: 2,
|
||||
});
|
||||
expect(next.squareContents[cellKey(cell)]).toEqual(expect.objectContaining({ kind: "rosebush", damage: 2 }));
|
||||
expect(next.players.find((p) => p.id === me.id)!.life).toBe(15);
|
||||
expect(next.groundObjects[cellKey(cell)]).toBeUndefined();
|
||||
expect(next.discard.some((c) => c.instanceId === rock.instanceId)).toBe(true);
|
||||
});
|
||||
|
||||
it("only fire hurts the ooze", () => {
|
||||
|
||||
Reference in New Issue
Block a user