The reflected-attack window (rules rev 25)

A FULL REFLECTION's returned spell is a fresh attack on its own caster —
who, per the owner's ruling, gets a defender's counteraction window
against it (absorb the blast, shield it, or FULL REFLECTION again: the
ping-pong war ends when the cards run out). The reflection now settles
into a new stack with the roles swapped and the damage pre-priced
(numbers, amplifies, and surviving counters already applied); the blow
returns to its sender unerringly, with no fresh hit rolls. onResolved
effects run with the swapped roles, so a reflected IDIOT now afflicts
its own caster. Earlier revisions land the blow instantly and stored
games replay unchanged.

Automatons price the incoming reflected damage off the stack; the attack
modal announces 'Your own spell comes screaming back!' with the inbound
total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-17 22:02:16 -04:00
co-authored by Claude Fable 5
parent dcaf358a93
commit 06faea0c79
6 changed files with 130 additions and 15 deletions
+61
View File
@@ -962,3 +962,64 @@ describe("empathy mirrors resolution-hook blows (rules rev 16)", () => {
expect(15 - aAfter.life).toBe(15 - dAfter.life);
});
});
describe("the reflected-attack window (rules rev 25)", () => {
function reflectRig() {
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic"], deckRev: 25 });
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const sd = giveCard(state, attacker, "sudden-death");
giveCard(state, defender, "full-reflection", "FR", 0);
state = must(state, attacker, {
type: "cast", instanceId: sd.instanceId, target: { kind: "player", playerId: defender },
});
state = must(state, defender, { type: "counteract", instanceId: "full-reflection#FR" });
// The attacker's pass meets the total stop and settles the reflection —
// which opens the returned spell's window, waiting on the caster.
state = must(state, attacker, { type: "pass" });
return { state, attacker, defender };
}
it("the returned spell opens a window: absorb soaks the reflected blast", () => {
let { state, attacker, defender } = reflectRig();
// The reflection settled into a NEW stack: the caster now defends.
expect(state.stack).not.toBeNull();
expect(state.stack!.defenderId).toBe(attacker);
expect(state.stack!.reflectedBase).toEqual({ damage: 10, duration: 0 });
giveCard(state, attacker, "absorb", "AB", 0);
state = must(state, attacker, { type: "counteract", instanceId: "absorb#AB" });
state = must(state, defender, { type: "pass" });
state = must(state, attacker, { type: "pass" });
// 10 reflected minus absorb's 3.
expect(state.players.find((p) => p.id === attacker)!.life).toBe(8);
expect(state.players.find((p) => p.id === defender)!.life).toBe(15);
});
it("a second full reflection turns it around again — the ping-pong war", () => {
let { state, attacker, defender } = reflectRig();
giveCard(state, attacker, "full-reflection", "FR2", 0);
state = must(state, attacker, { type: "counteract", instanceId: "full-reflection#FR2" });
state = must(state, defender, { type: "pass" });
// Turned around again: now the original defender must answer it.
expect(state.stack).not.toBeNull();
expect(state.stack!.defenderId).toBe(defender);
state = must(state, defender, { type: "pass" });
expect(state.players.find((p) => p.id === defender)!.life).toBe(5);
expect(state.players.find((p) => p.id === attacker)!.life).toBe(15);
});
it("rev 24 lands the reflected blow instantly, as stored games replay", () => {
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic"], deckRev: 24 });
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const sd = giveCard(state, attacker, "sudden-death");
giveCard(state, defender, "full-reflection", "FR", 0);
state = must(state, attacker, {
type: "cast", instanceId: sd.instanceId, target: { kind: "player", playerId: defender },
});
state = must(state, defender, { type: "counteract", instanceId: "full-reflection#FR" });
state = must(state, attacker, { type: "pass" });
expect(state.stack).toBeNull();
expect(state.players.find((p) => p.id === attacker)!.life).toBe(5);
});
});