Rev 26: BUTT-HEAD's ram is priced into the blow, so STRENGTH, AMPLIFY, and the counters weigh it (KWUZ)
Ramses cast STRENGTH, then an amplified BUTT-HEAD three squares off, and the goat dealt three: the ram was applied in onResolved, past the pipe that doubles, soaks, and adds. Older games keep the bare distance. 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
5193b12b8e
commit
31bfcf7bc0
@@ -247,7 +247,7 @@ export interface CastParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
||||||
export const CURRENT_RULES_REV = 25;
|
export const CURRENT_RULES_REV = 26;
|
||||||
|
|
||||||
/** Every rulings revision since the baseline, newest last — the entries a
|
/** 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
|
* game's deckRev freezes it before or after. Shown to players as the house
|
||||||
@@ -277,6 +277,7 @@ export const RULES_REVISIONS: { rev: number; note: string }[] = [
|
|||||||
{ 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: 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: 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." },
|
{ 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." },
|
||||||
|
{ rev: 26, note: "BUTT-HEAD's ram is physical damage like any other: STRENGTH doubles it, AMPLIFY doubles it, WEAKNESS doubles it, POWER ATTACK adds to it, and ABSORB or BLUNT soak it. Older games dealt the bare distance, whatever rode the cast." },
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface GameConfig {
|
export interface GameConfig {
|
||||||
@@ -2452,7 +2453,10 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
|||||||
ctx.state.turn.movementUsed += d;
|
ctx.state.turn.movementUsed += d;
|
||||||
ctx.attacker.position = { ...ctx.defender.position };
|
ctx.attacker.position = { ...ctx.defender.position };
|
||||||
ctx.events.push({ type: "rammed", attacker: ctx.attacker.id, target: ctx.defender.id, distance: d });
|
ctx.events.push({ type: "rammed", attacker: ctx.attacker.id, target: ctx.defender.id, distance: d });
|
||||||
resolvedBlow(ctx, d, "goat ram", "physical");
|
// Rev 26: the ram was priced into the blow itself (ramDistance),
|
||||||
|
// so STRENGTH, AMPLIFY, and the counters have already weighed it.
|
||||||
|
// Older games dealt the bare distance here, past all of them.
|
||||||
|
if ((ctx.state.config.deckRev ?? 1) < 26) resolvedBlow(ctx, d, "goat ram", "physical");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const d = Math.abs(ctx.defender.position.x - ctx.attacker.position.x) +
|
const d = Math.abs(ctx.defender.position.x - ctx.attacker.position.x) +
|
||||||
@@ -6035,6 +6039,14 @@ function checkAmbushes(
|
|||||||
/** Damage dealt inside an attack's own resolution hook still honors
|
/** Damage dealt inside an attack's own resolution hook still honors
|
||||||
* EMPATHY — "Any attack done in ANY form against you acts against both you
|
* EMPATHY — "Any attack done in ANY form against you acts against both you
|
||||||
* and the caster". */
|
* and the caster". */
|
||||||
|
/** How far the goat charges: the corridor distance to its mark within
|
||||||
|
* this turn's legs, or zero when the mark is underfoot or out of reach. */
|
||||||
|
function ramDistance(state: GameState, attacker: PlayerState, defender: PlayerState): number {
|
||||||
|
const legs = state.turn.movementAllowance - state.turn.movementUsed;
|
||||||
|
const d = walkingDistance(state, attacker.position, defender.position, chargeReach(state, legs));
|
||||||
|
return d > legs ? 0 : d;
|
||||||
|
}
|
||||||
|
|
||||||
function resolvedBlow(
|
function resolvedBlow(
|
||||||
ctx: ResolutionContext, dmg: number, source: string, kind?: "physical",
|
ctx: ResolutionContext, dmg: number, source: string, kind?: "physical",
|
||||||
): void {
|
): void {
|
||||||
@@ -6371,6 +6383,9 @@ function resolveStack(state: GameState, events: GameEvent[]): void {
|
|||||||
isMagicStone(c.cardId) && defender.displayed.includes(c.instanceId));
|
isMagicStone(c.cardId) && defender.displayed.includes(c.instanceId));
|
||||||
base = (stack.numberValue ?? 1) * inPlay.length;
|
base = (stack.numberValue ?? 1) * inPlay.length;
|
||||||
}
|
}
|
||||||
|
// Rev 26: BUTT-HEAD's ram is priced here, as the physical blow it is,
|
||||||
|
// so everything below weighs it like any other.
|
||||||
|
if (attackId === "butt-head" && (state.config.deckRev ?? 1) >= 26) base = ramDistance(state, attacker, defender);
|
||||||
base *= stack.amplifyFactor;
|
base *= stack.amplifyFactor;
|
||||||
base += stack.powerAttackPoints;
|
base += stack.powerAttackPoints;
|
||||||
// SWARTHMORE'S ENCHANTMENT: an enchanted thrown object bites one deeper.
|
// SWARTHMORE'S ENCHANTMENT: an enchanted thrown object bites one deeper.
|
||||||
|
|||||||
@@ -895,3 +895,47 @@ describe("cards that keep their whole promise (rev 21)", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("the goat's ram is a physical blow like any other (rev 26)", () => {
|
||||||
|
/** A goat and its mark a charge apart, the goat holding STRENGTH, AMPLIFY, and the ram. */
|
||||||
|
function charged(deckRev?: number) {
|
||||||
|
let { state } = createGame({ playerIds: ["goat", "mark"], seed: 42, sets: ["basic", "expansion1"], ...(deckRev ? { deckRev } : {}) });
|
||||||
|
state = toRound2(state);
|
||||||
|
const goat = activePlayer(state);
|
||||||
|
const mark = state.players.find((p) => p.id !== goat.id)!;
|
||||||
|
const bh = giveCard(state, goat.id, "butt-head");
|
||||||
|
giveCard(state, goat.id, "amplify", "A", 1);
|
||||||
|
giveCard(state, goat.id, "strength", "S", 2);
|
||||||
|
giveCard(state, goat.id, "number-2", "N", 3);
|
||||||
|
state = must(state, goat.id, { type: "cast", instanceId: "strength#S", numberInstanceIds: ["number-2#N"] });
|
||||||
|
// Any square the charge reaches at two or more steps will do.
|
||||||
|
for (const key of Object.keys(boardView(state).cells)) {
|
||||||
|
if (key === cellKey(goat.position)) continue;
|
||||||
|
const [x, y] = key.split(",").map(Number) as [number, number];
|
||||||
|
const trial = structuredClone(state);
|
||||||
|
trial.players.find((p) => p.id === mark.id)!.position = { x, y };
|
||||||
|
const r = applyCommand(trial, goat.id, {
|
||||||
|
type: "cast", instanceId: bh.instanceId, target: { kind: "player", playerId: mark.id }, amplifyInstanceIds: ["amplify#A"],
|
||||||
|
});
|
||||||
|
if (!r.ok) continue;
|
||||||
|
const done = applyCommand(r.state, mark.id, { type: "pass" });
|
||||||
|
if (!done.ok) continue;
|
||||||
|
const ram = done.events.find((e) => e.type === "rammed");
|
||||||
|
if (!ram || ram.type !== "rammed" || ram.distance < 2) continue;
|
||||||
|
return { events: done.events, distance: ram.distance, state: done.state, goat: goat.id, mark: mark.id };
|
||||||
|
}
|
||||||
|
throw new Error("no square a charge of two or more reaches on this board");
|
||||||
|
}
|
||||||
|
|
||||||
|
it("STRENGTH and AMPLIFY each double the ram", () => {
|
||||||
|
const { events, distance, state, mark } = charged();
|
||||||
|
expect(events).toContainEqual(expect.objectContaining({ type: "damaged", player: mark, amount: distance * 4 }));
|
||||||
|
expect(state.players.find((p) => p.id === mark)!.life).toBe(15 - distance * 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("older games dealt the bare distance, whatever rode the cast", () => {
|
||||||
|
const { events, distance, state, mark } = charged(25);
|
||||||
|
expect(events).toContainEqual(expect.objectContaining({ type: "damaged", player: mark, amount: distance }));
|
||||||
|
expect(state.players.find((p) => p.id === mark)!.life).toBe(15 - distance);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user