Reflected permanent curses return to their caster (rules rev 11)
SLOW DEATH, WALKING DEAD, and IDIOT ride no damage or duration pipe, so a FULL REFLECTION used to evaporate them; per the FAQ reflections act on the cast, so the returned curse now opens its stack against the caster like any reflected spell. Marked permanentCurse on the three defs; older ledgers keep the evaporation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
e885325098
commit
dd6ba59220
@@ -271,7 +271,9 @@ export interface GameConfig {
|
|||||||
* corridors on the turn's legal legs (three plus numbers) and spends
|
* corridors on the turn's legal legs (three plus numbers) and spends
|
||||||
* them.
|
* them.
|
||||||
* Rev 11: a REVERSE against SLOW DEATH turns the whole curse (FAQ) —
|
* Rev 11: a REVERSE against SLOW DEATH turns the whole curse (FAQ) —
|
||||||
* the victim gains a point for every card drawn, permanently.
|
* the victim gains a point for every card drawn, permanently — and a
|
||||||
|
* FULL REFLECTION returns a permanent curse (SLOW DEATH, WALKING DEAD,
|
||||||
|
* IDIOT) onto its caster instead of letting it evaporate.
|
||||||
*/
|
*/
|
||||||
deckRev?: number;
|
deckRev?: number;
|
||||||
}
|
}
|
||||||
@@ -826,6 +828,10 @@ type AttackEffect = {
|
|||||||
baseDamage: (numberValue: number | null, params: CastParams | null) => number;
|
baseDamage: (numberValue: number | null, params: CastParams | null) => number;
|
||||||
/** A duration spell: attach a sustained effect on resolution. */
|
/** A duration spell: attach a sustained effect on resolution. */
|
||||||
sustains?: boolean;
|
sustains?: boolean;
|
||||||
|
/** A permanent curse attached by onResolved (SLOW DEATH, WALKING DEAD,
|
||||||
|
* IDIOT): it rides no damage or duration pipe, so a FULL REFLECTION
|
||||||
|
* must return it explicitly or the curse would evaporate. */
|
||||||
|
permanentCurse?: true;
|
||||||
/** Card stays in hand and is displayed rather than discarded (WIZARDBLADE). */
|
/** Card stays in hand and is displayed rather than discarded (WIZARDBLADE). */
|
||||||
keepInHand?: boolean;
|
keepInHand?: boolean;
|
||||||
validate?: (state: GameState, cmd: Extract<Command, { type: "cast" }>, caster?: PlayerState, target?: PlayerState) => string | null;
|
validate?: (state: GameState, cmd: Extract<Command, { type: "cast" }>, caster?: PlayerState, target?: PlayerState) => string | null;
|
||||||
@@ -1720,6 +1726,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
|||||||
kind: "attack",
|
kind: "attack",
|
||||||
requiresLos: true,
|
requiresLos: true,
|
||||||
baseDamage: () => 0,
|
baseDamage: () => 0,
|
||||||
|
permanentCurse: true,
|
||||||
// "This is permanent. Once SLOW DEATH is on, it can't be turned off."
|
// "This is permanent. Once SLOW DEATH is on, it can't be turned off."
|
||||||
// Rev 11, per the FAQ: "If SLOW DEATH is REVERSED, the player receives
|
// Rev 11, per the FAQ: "If SLOW DEATH is REVERSED, the player receives
|
||||||
// a point for every card drawn" — the whole curse attaches backward,
|
// a point for every card drawn" — the whole curse attaches backward,
|
||||||
@@ -2210,6 +2217,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
|||||||
kind: "attack",
|
kind: "attack",
|
||||||
requiresLos: true,
|
requiresLos: true,
|
||||||
baseDamage: () => 0,
|
baseDamage: () => 0,
|
||||||
|
permanentCurse: true,
|
||||||
// "1/2 point of damage for every space moved. This spell is permanent."
|
// "1/2 point of damage for every space moved. This spell is permanent."
|
||||||
onResolved: (ctx) => {
|
onResolved: (ctx) => {
|
||||||
if (ctx.fullyStopped || !ctx.defender.alive) return;
|
if (ctx.fullyStopped || !ctx.defender.alive) return;
|
||||||
@@ -2554,6 +2562,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
|||||||
kind: "attack",
|
kind: "attack",
|
||||||
requiresLos: true,
|
requiresLos: true,
|
||||||
baseDamage: () => 0,
|
baseDamage: () => 0,
|
||||||
|
permanentCurse: true,
|
||||||
// "Opponent heads straight for the nearest of his own treasures ... This
|
// "Opponent heads straight for the nearest of his own treasures ... This
|
||||||
// lasts until opponent is on his own treasure."
|
// lasts until opponent is on his own treasure."
|
||||||
sustains: false,
|
sustains: false,
|
||||||
@@ -6043,9 +6052,11 @@ function resolveStack(state: GameState, events: GameEvent[]): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The returned spell is a fresh attack on its own caster — who gets a
|
// The returned spell is a fresh attack on its own caster — who gets a
|
||||||
// defender's counteraction window against it.
|
// defender's counteraction window against it. Rev 11: permanent curses
|
||||||
|
// ride the return too; older games let a reflected curse evaporate.
|
||||||
|
const returnsCurse = effect?.permanentCurse === true && (state.config.deckRev ?? 1) >= 11;
|
||||||
if (!stack.trapped && !attackingCreature &&
|
if (!stack.trapped && !attackingCreature &&
|
||||||
attacker.alive && (pipe.damage > 0 || pipe.duration > 0)) {
|
attacker.alive && (pipe.damage > 0 || pipe.duration > 0 || returnsCurse)) {
|
||||||
state.stack = {
|
state.stack = {
|
||||||
attackerId: defender.id,
|
attackerId: defender.id,
|
||||||
defenderId: attacker.id,
|
defenderId: attacker.id,
|
||||||
|
|||||||
@@ -170,6 +170,26 @@ describe("slow death", () => {
|
|||||||
expect(state.players.find((p) => p.id === defender)!.life).toBe(17);
|
expect(state.players.find((p) => p.id === defender)!.life).toBe(17);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("a full reflection returns the curse onto its caster (rev 11)", () => {
|
||||||
|
let { state } = newGame();
|
||||||
|
state = toRound2(state);
|
||||||
|
const { attacker, defender } = faceOff(state);
|
||||||
|
const sd = giveCard(state, attacker, "slow-death");
|
||||||
|
giveCard(state, defender, "full-reflection", "FR", 0);
|
||||||
|
const r = applyCommand(state, attacker, {
|
||||||
|
type: "cast", instanceId: sd.instanceId, target: { kind: "player", playerId: defender },
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(r.error);
|
||||||
|
state = must(r.state, defender, { type: "counteract", instanceId: "full-reflection#FR" });
|
||||||
|
state = drain(state);
|
||||||
|
expect(sustainedOn(state, defender, "slow-death").length).toBe(0);
|
||||||
|
expect(sustainedOn(state, attacker, "slow-death").length).toBe(1);
|
||||||
|
|
||||||
|
// The caster now bleeds for their own draws.
|
||||||
|
state = must(state, attacker, { type: "endTurn", draw: 1 });
|
||||||
|
expect(state.players.find((p) => p.id === attacker)!.life).toBe(14);
|
||||||
|
});
|
||||||
|
|
||||||
it("older decks attach the biting curse through a reverse (rev ≤10)", () => {
|
it("older decks attach the biting curse through a reverse (rev ≤10)", () => {
|
||||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 10 });
|
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 10 });
|
||||||
state = toRound2(state);
|
state = toRound2(state);
|
||||||
|
|||||||
Reference in New Issue
Block a user