Zero-damage attacks stop lying about being stopped
DROP OBJECT resolved correctly — the treasure fell — but the chronicle announced "The attack is completely stopped," because the attackResolved event branded any attack that dealt zero damage as stopped. Utility attacks deal zero by design; only an attack that was TRYING to wound counts as stopped when nothing lands. Tests pin both sides: uncountered DROP OBJECT reports unstopped and drops the treasure; FULL SHIELD reports stopped and the treasure stays carried. Not deployed — a live game is in progress. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7e3e00bf97
commit
b210cffa76
@@ -4497,6 +4497,9 @@ function resolveStack(state: GameState, events: GameEvent[]): void {
|
|||||||
? (stack.numberValue ?? 1) * stack.amplifyFactor * stack.extendFactor
|
? (stack.numberValue ?? 1) * stack.amplifyFactor * stack.extendFactor
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
// Whether this attack even tries to wound: utility attacks (DROP OBJECT,
|
||||||
|
// TELEPORT OPPONENT) deal 0 by design, and dealing 0 is not being stopped.
|
||||||
|
const dealsDamage = base > 0;
|
||||||
const pipe: DamagePipeline = {
|
const pipe: DamagePipeline = {
|
||||||
damage: base,
|
damage: base,
|
||||||
duration: baseDuration,
|
duration: baseDuration,
|
||||||
@@ -4578,7 +4581,7 @@ function resolveStack(state: GameState, events: GameEvent[]): void {
|
|||||||
attackCardId: attackId,
|
attackCardId: attackId,
|
||||||
damageDealt,
|
damageDealt,
|
||||||
reflectedDamage: pipe.redirected ? pipe.damage : pipe.reflectedDamage,
|
reflectedDamage: pipe.redirected ? pipe.damage : pipe.reflectedDamage,
|
||||||
fullyStopped: pipe.fullyStopped || (damageDealt === 0 && !pipe.redirected && !effect?.sustains),
|
fullyStopped: pipe.fullyStopped || (dealsDamage && damageDealt === 0 && !pipe.redirected && !effect?.sustains),
|
||||||
redirected: pipe.redirected,
|
redirected: pipe.redirected,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -487,3 +487,50 @@ describe("rules revision 3", () => {
|
|||||||
expect(state.players.find((p) => p.id === thief.id)!.life).toBe(12);
|
expect(state.players.find((p) => p.id === thief.id)!.life).toBe(12);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("zero-damage utility attacks", () => {
|
||||||
|
it("an uncountered DROP OBJECT is not reported as stopped — and the treasure falls", () => {
|
||||||
|
let { state } = newGame();
|
||||||
|
state = toRound2(state);
|
||||||
|
const { attacker, defender } = faceOff(state);
|
||||||
|
const d = state.players.find((p) => p.id === defender)!;
|
||||||
|
const treasure = state.treasures.find((t) => t.owner !== defender)!;
|
||||||
|
treasure.carriedBy = defender;
|
||||||
|
treasure.position = null;
|
||||||
|
d.carriedTreasureId = treasure.id;
|
||||||
|
const dob = giveCard(state, attacker, "drop-object");
|
||||||
|
state = must(state, attacker, {
|
||||||
|
type: "cast", instanceId: dob.instanceId, params: { cardId: "treasure" },
|
||||||
|
target: { kind: "player", playerId: defender },
|
||||||
|
});
|
||||||
|
const result = applyCommand(state, defender, { type: "pass" });
|
||||||
|
if (!result.ok) throw new Error(result.error);
|
||||||
|
const resolved = result.events.find((e) => e.type === "attackResolved");
|
||||||
|
expect(resolved && "fullyStopped" in resolved && resolved.fullyStopped).toBe(false);
|
||||||
|
expect(result.state.players.find((p) => p.id === defender)!.carriedTreasureId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FULL SHIELD genuinely stops it — the treasure stays carried", () => {
|
||||||
|
let { state } = newGame();
|
||||||
|
state = toRound2(state);
|
||||||
|
const { attacker, defender } = faceOff(state);
|
||||||
|
const d = state.players.find((p) => p.id === defender)!;
|
||||||
|
const treasure = state.treasures.find((t) => t.owner !== defender)!;
|
||||||
|
treasure.carriedBy = defender;
|
||||||
|
treasure.position = null;
|
||||||
|
d.carriedTreasureId = treasure.id;
|
||||||
|
const dob = giveCard(state, attacker, "drop-object");
|
||||||
|
giveCard(state, defender, "full-shield", "FS", 0);
|
||||||
|
state = must(state, attacker, {
|
||||||
|
type: "cast", instanceId: dob.instanceId, params: { cardId: "treasure" },
|
||||||
|
target: { kind: "player", playerId: defender },
|
||||||
|
});
|
||||||
|
state = must(state, defender, { type: "counteract", instanceId: "full-shield#FS" });
|
||||||
|
state = must(state, attacker, { type: "pass" });
|
||||||
|
const result = applyCommand(state, defender, { type: "pass" });
|
||||||
|
if (!result.ok) throw new Error(result.error);
|
||||||
|
const resolved = result.events.find((e) => e.type === "attackResolved");
|
||||||
|
expect(resolved && "fullyStopped" in resolved && resolved.fullyStopped).toBe(true);
|
||||||
|
expect(result.state.players.find((p) => p.id === defender)!.carriedTreasureId).toBe(treasure.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user