Rev 27: an ILLUSIONARY ATTACK is priced as the spell it fakes, so BLUNT and the rest meet it (UPJE)
The believed illusion was dealt after the counter trail had already spoken over a zero, so a BLUNT trailed for nothing and the full points landed. Now the fake carries its spell's points through the pipeline and the belief roll decides whether the weighed blow lands. Games dealt under rev 26 and before replay as they were. 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
991a6a0443
commit
c3f63d4eb7
@@ -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 = 26;
|
export const CURRENT_RULES_REV = 27;
|
||||||
|
|
||||||
/** 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
|
||||||
@@ -278,6 +278,7 @@ export const RULES_REVISIONS: { rev: number; note: string }[] = [
|
|||||||
{ 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." },
|
{ 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." },
|
||||||
|
{ rev: 27, note: "An ILLUSIONARY ATTACK is priced as the spell it fakes, so a BLUNT halves it, an ABSORB soaks it, and a shield stops it, exactly as against the real thing; the belief roll then decides whether the weighed blow lands at all. Older games dealt a believed illusion's full points past every counteraction." },
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface GameConfig {
|
export interface GameConfig {
|
||||||
@@ -2542,6 +2543,8 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onResolved: (ctx) => {
|
onResolved: (ctx) => {
|
||||||
|
// Rev 27 prices the illusion into the pipeline and rolls belief there.
|
||||||
|
if ((ctx.state.config.deckRev ?? 1) >= 27) return;
|
||||||
if (ctx.fullyStopped || !ctx.defender.alive) return;
|
if (ctx.fullyStopped || !ctx.defender.alive) return;
|
||||||
const chosen = ctx.stack.params!.cardId!;
|
const chosen = ctx.stack.params!.cardId!;
|
||||||
const fx = CARD_EFFECTS[chosen] as AttackEffect;
|
const fx = CARD_EFFECTS[chosen] as AttackEffect;
|
||||||
@@ -6386,6 +6389,13 @@ function resolveStack(state: GameState, events: GameEvent[]): void {
|
|||||||
// Rev 26: BUTT-HEAD's ram is priced here, as the physical blow it is,
|
// Rev 26: BUTT-HEAD's ram is priced here, as the physical blow it is,
|
||||||
// so everything below weighs it like any other.
|
// so everything below weighs it like any other.
|
||||||
if (attackId === "butt-head" && (state.config.deckRev ?? 1) >= 26) base = ramDistance(state, attacker, defender);
|
if (attackId === "butt-head" && (state.config.deckRev ?? 1) >= 26) base = ramDistance(state, attacker, defender);
|
||||||
|
// Rev 27: an ILLUSIONARY ATTACK is priced as the spell it fakes, so the
|
||||||
|
// trail weighs it like the real thing. Whether the weighed blow lands at
|
||||||
|
// all is the belief roll's, taken below once the trail has spoken.
|
||||||
|
if (attackId === "illusionary-attack" && (state.config.deckRev ?? 1) >= 27 && stack.params?.cardId) {
|
||||||
|
const faked = CARD_EFFECTS[stack.params.cardId];
|
||||||
|
if (faked?.kind === "attack") base = faked.baseDamage(stack.numberValue, stack.params);
|
||||||
|
}
|
||||||
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.
|
||||||
@@ -6482,6 +6492,23 @@ function resolveStack(state: GameState, events: GameEvent[]): void {
|
|||||||
}
|
}
|
||||||
weigh();
|
weigh();
|
||||||
|
|
||||||
|
// Rev 27: "Your opponent has a 50% chance of believing the spell to be
|
||||||
|
// real. If so, he takes damage from the spell" — the weighed damage, the
|
||||||
|
// counteractions having met the illusion as they would the real spell.
|
||||||
|
// Disbelief leaves nothing to land, and a total stop leaves nothing to
|
||||||
|
// believe in, so no die is rolled then.
|
||||||
|
if (attackId === "illusionary-attack" && (state.config.deckRev ?? 1) >= 27 && !stack.reflectedBase &&
|
||||||
|
(pipe.damage > 0 || pipe.reflectedDamage > 0 || pipe.duration > 0)) {
|
||||||
|
const [roll, rngNext] = rollDie(state.rng);
|
||||||
|
state.rng = rngNext;
|
||||||
|
const believed = roll <= 2;
|
||||||
|
events.push({ type: "illusionBelieved", player: defender.id, cardId: stack.params!.cardId!, believed });
|
||||||
|
if (!believed) {
|
||||||
|
pipe.damage = 0;
|
||||||
|
pipe.reflectedDamage = 0;
|
||||||
|
pipe.duration = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A surviving teleport counter whisks the defender away before anything lands.
|
// A surviving teleport counter whisks the defender away before anything lands.
|
||||||
const escape = stack.counters.find((c) => !c.nullified && c.card.cardId === "teleport" && c.cell);
|
const escape = stack.counters.find((c) => !c.nullified && c.card.cardId === "teleport" && c.cell);
|
||||||
|
|||||||
@@ -939,3 +939,55 @@ describe("the goat's ram is a physical blow like any other (rev 26)", () => {
|
|||||||
expect(state.players.find((p) => p.id === mark)!.life).toBe(15 - distance);
|
expect(state.players.find((p) => p.id === mark)!.life).toBe(15 - distance);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("an illusion is priced as the spell it fakes (rev 27)", () => {
|
||||||
|
/** A faked FIREBALL met by the given counteraction, then both wizards pass. */
|
||||||
|
function fakedFireball(counter: string | null, deckRev?: number) {
|
||||||
|
let { state } = createGame({ playerIds: ["liar", "mark"], seed: 42, sets: ["basic", "expansion1"], ...(deckRev ? { deckRev } : {}) });
|
||||||
|
state = toRound2(state);
|
||||||
|
const { attacker, defender } = faceOff(state);
|
||||||
|
const ill = giveCard(state, attacker, "illusionary-attack");
|
||||||
|
if (counter) giveCard(state, defender, counter, "C", 0);
|
||||||
|
state = must(state, attacker, {
|
||||||
|
type: "cast", instanceId: ill.instanceId, target: { kind: "player", playerId: defender }, params: { cardId: "fireball" },
|
||||||
|
});
|
||||||
|
let done = applyCommand(state, defender, { type: "pass" });
|
||||||
|
if (counter) {
|
||||||
|
state = must(state, defender, { type: "counteract", instanceId: `${counter}#C` });
|
||||||
|
// The attacker's pass settles a total stop at once; otherwise the
|
||||||
|
// defender's pass closes the window.
|
||||||
|
done = applyCommand(state, attacker, { type: "pass" });
|
||||||
|
if (done.ok && done.state.stack) done = applyCommand(done.state, defender, { type: "pass" });
|
||||||
|
}
|
||||||
|
if (!done.ok) throw new Error(done.error);
|
||||||
|
const belief = done.events.find((e) => e.type === "illusionBelieved");
|
||||||
|
const believed = belief?.type === "illusionBelieved" ? belief.believed : null;
|
||||||
|
return { events: done.events, believed, life: done.state.players.find((p) => p.id === defender)!.life };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("BLUNT halves a believed fake fireball to three, and disbelief costs nothing", () => {
|
||||||
|
const { events, believed, life } = fakedFireball("blunt");
|
||||||
|
expect(believed).not.toBeNull();
|
||||||
|
expect(events).toContainEqual(expect.objectContaining({ type: "attackResolved", incoming: 5 }));
|
||||||
|
expect(life).toBe(believed ? 12 : 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FULL SHIELD stops the fake outright: nothing to believe in, no die rolled", () => {
|
||||||
|
const { events, believed, life } = fakedFireball("full-shield");
|
||||||
|
expect(believed).toBeNull();
|
||||||
|
expect(events).toContainEqual(expect.objectContaining({ type: "attackResolved", fullyStopped: true }));
|
||||||
|
expect(life).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uncountered, a believed fake fireball still does its five", () => {
|
||||||
|
const { believed, life } = fakedFireball(null);
|
||||||
|
expect(believed).not.toBeNull();
|
||||||
|
expect(life).toBe(believed ? 10 : 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("older games dealt a believed illusion's full points past a BLUNT", () => {
|
||||||
|
const { believed, life } = fakedFireball("blunt", 26);
|
||||||
|
expect(believed).not.toBeNull();
|
||||||
|
expect(life).toBe(believed ? 10 : 15);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user