Rework sacrifice effects to support "can't be sacrificed" (#11587)

* add TargetSacrifice and canBeSacrificed

* SacrificeTargetCost refactor, now uses TargetSacrifice, constructors simplified, subclasses aligned

* fix text errors introduced by refactor

* refactor SacrificeEffect, SacrificeAllEffect, SacrificeOpponentsEffect

* cleanup keyword abilities involving sacrifice

* fix a bunch of custom effect classes involving sacrifice

* fix test choices

* update Assault Suit implementation

* fix filter check arguments

* add documentation to refactored common classes

* [CLB] Implement Jon Irenicus, Shattered One

* implement "{this} can't be sacrificed"

* add tests for Assault Suit and Jon Irenicus

* refactor out PlayerToRightGainsControlOfSourceEffect

* implement [LTC] Hithlain Rope

* add choose hint to all TargetSacrifice

---------

Co-authored-by: Evan Kranzler <theelk801@gmail.com>
Co-authored-by: PurpleCrowbar <26198472+PurpleCrowbar@users.noreply.github.com>
This commit is contained in:
xenohedron 2023-12-31 14:10:37 -05:00 committed by GitHub
parent f28c5c4fc5
commit 9b3ff32a33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
699 changed files with 1837 additions and 1619 deletions

View file

@ -24,7 +24,7 @@ public class TargetRequiredTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Redcap Melee", "Silvercoat Lion");
addTarget(playerA, "Mountain");
setChoice(playerA, "Mountain");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);

View file

@ -65,7 +65,7 @@ public class ValakutTheMoltenPinnacleTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Scapeshift");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scapeshift");
addTarget(playerA, "Forest^Forest^Forest^Forest^Forest^Forest");
setChoice(playerA, "Forest^Forest^Forest^Forest^Forest^Forest");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
@ -88,7 +88,7 @@ public class ValakutTheMoltenPinnacleTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Scapeshift");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scapeshift");
addTarget(playerA, "Forest^Forest^Forest^Forest^Forest^Forest^Forest");
setChoice(playerA, "Forest^Forest^Forest^Forest^Forest^Forest^Forest");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
@ -113,7 +113,7 @@ public class ValakutTheMoltenPinnacleTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Scapeshift");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scapeshift");
addTarget(playerA, "Forest^Forest^Forest^Forest^Forest^Forest^Forest");
setChoice(playerA, "Forest^Forest^Forest^Forest^Forest^Forest^Forest");
setChoice(playerA, false); // Stomping Ground can be tapped
setChoice(playerA, false); // Stomping Ground can be tapped
setChoice(playerA, false); // Stomping Ground can be tapped

View file

@ -217,7 +217,7 @@ public class BestowTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "fused Far // Away");
addTarget(playerB, "Cyclops of One-Eyed Pass"); // Far
addTarget(playerB, playerA); // Away
addTarget(playerA, "Nyxborn Rollicker");
setChoice(playerA, "Nyxborn Rollicker");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);

View file

@ -33,7 +33,7 @@ public class ExploitTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Silumgar Butcher");
setChoice(playerA, true);
addTarget(playerA, "Silvercoat Lion"); // sacrifice to Exploit
setChoice(playerA, "Silvercoat Lion"); // sacrifice to Exploit
addTarget(playerA, "Thundering Giant"); // Target for the -3/-3
setStopAt(1, PhaseStep.BEGIN_COMBAT);
@ -66,7 +66,7 @@ public class ExploitTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Silumgar Butcher");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
setChoice(playerA, true); // Choose to exploit
addTarget(playerA, "Silvercoat Lion"); // sacrifice to Exploit
setChoice(playerA, "Silvercoat Lion"); // sacrifice to Exploit
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silumgar Butcher");
@ -94,7 +94,7 @@ public class ExploitTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Qarsi Sadist");
setChoice(playerA, true);
addTarget(playerA, "Qarsi Sadist"); // sacrifice to Exploit
setChoice(playerA, "Qarsi Sadist"); // sacrifice to Exploit
// Player B is auto-chosen to lose two life since only option
setStopAt(1, PhaseStep.BEGIN_COMBAT);
@ -105,4 +105,4 @@ public class ExploitTest extends CardTestPlayerBase {
assertLife(playerA, 22);
assertLife(playerB, 18);
}
}
}

View file

@ -324,7 +324,7 @@ public class KickerTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gatekeeper of Malakir");
setChoice(playerA, true); // use kicker
addTarget(playerA, playerB); // trigger's target
addTarget(playerB, "Birds of Paradise"); // sacrifice
setChoice(playerB, "Birds of Paradise"); // sacrifice
// return to hand
castSpell(1, PhaseStep.BEGIN_COMBAT, playerB, "Boomerang", "Gatekeeper of Malakir");

View file

@ -19,7 +19,7 @@ public class OfferingTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, nezumiPatron);
setChoice(playerA, true);
addTarget(playerA, kurosTaken);
setChoice(playerA, kurosTaken);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
@ -64,7 +64,7 @@ public class OfferingTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, patron);
setChoice(playerA, true);
addTarget(playerA, "Akki Drillmaster");
setChoice(playerA, "Akki Drillmaster");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
@ -90,7 +90,7 @@ public class OfferingTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, patron);
setChoice(playerA, true);
addTarget(playerA, "Boros Recruit");
setChoice(playerA, "Boros Recruit");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
@ -117,7 +117,7 @@ public class OfferingTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, patron);
setChoice(playerA, true);
addTarget(playerA, "Boggart Ram-Gang");
setChoice(playerA, "Boggart Ram-Gang");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
@ -138,7 +138,7 @@ public class OfferingTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Blast-Furnace Hellkite");
setChoice(playerA, true); // use offering
addTarget(playerA, "Ancient Den");
setChoice(playerA, "Ancient Den");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);

View file

@ -27,7 +27,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase {
/**
* Omniscience only lets you cast spells for free from your hand.
* Haakon lets you cast knights from your graveyard.
*
* <p>
* If you control both, you must still pay costs to cast knights from your graveyard.
*/
@Test
@ -196,14 +196,14 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase {
/**
* Omniscience is not allowing me to cast spells for free. I'm playing a
* Commander game against the Computer, if that helps.
*
* <p>
* Edit: It's not letting me cast fused spells for free. Others seems to be
* working.
*/
@Test
public void testCastingFusedSpell() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Omniscience");
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion");
@ -221,7 +221,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase {
setChoice(playerA, true); // Cast without paying its mana cost?
addTarget(playerA, "Silvercoat Lion");
addTarget(playerA, playerB);
playerB.addTarget("Pillarfield Ox");
setChoice(playerB, "Pillarfield Ox");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
@ -234,7 +234,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase {
assertPermanentCount(playerB, "Pillarfield Ox", 0);
assertGraveyardCount(playerB, "Pillarfield Ox", 1);
}
/**
* Omniscience only lets you cast spells from your hand without paying their mana costs.
@ -312,11 +312,11 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase {
* If a spell has an unpayable cost (e.g. Ancestral Vision, which has no mana cost),
* Omniscience should allow you to cast that spell without paying its mana cost.
* In the case of Ancestral Vision, for example, Xmage only gives you the option to suspend Ancestral Vision.
*
* <p>
* 118.6a If an unpayable cost is increased by an effect or an additional cost is imposed,
* the cost is still unpayable.
* If an alternative cost is applied to an unpayable cost, including an effect that allows a player
* to cast a spell without paying its mana cost, the alternative cost may be paid.
* the cost is still unpayable.
* If an alternative cost is applied to an unpayable cost, including an effect that allows a player
* to cast a spell without paying its mana cost, the alternative cost may be paid.
*/
@Test
public void testCastingUnpayableCost() {
@ -343,30 +343,30 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase {
// Not sure what the exact interaction is, but when Omniscience is on the field with Jodah,
// if you say "no" to the Jodah cast option to get to the Omniscience option, then the game will initiate a rollback.
@Test
public void test_OmniscienceAndJodah() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
// Flying
// You may pay WUBRG rather than pay the mana cost for spells that you cast.
addCard(Zone.BATTLEFIELD, playerA, "Jodah, Archmage Eternal"); // Creature {1}{U}{R}{W} (4/3)
// You may cast nonland cards from your hand without paying their mana costs.
addCard(Zone.HAND, playerA, "Omniscience"); // Enchantment {7}{U}{U}{U}
// Creature - 3/3 Swampwalk
addCard(Zone.HAND, playerA, "Bog Wraith", 1); // Creature {3}{B} (3/3)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Omniscience", true);
setChoice(playerA, true); // Pay alternative costs? ({W}{U}{B}{R}{G})
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bog Wraith");
// The order of the two alternate casting abilities is not fixed, so it's not clear which ability is asked for first
setChoice(playerA, false); // Pay alternative costs? ({W}{U}{B}{R}{G})

View file

@ -0,0 +1,117 @@
package org.mage.test.cards.rules;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author LevelX2
*/
public class CantBeSacrificedTest extends CardTestPlayerBase {
private static final String assaultSuit = "Assault Suit";
/*
Assault Suit {4}
Artifact Equipment
Equipped creature gets +2/+2, has haste, cant attack you or planeswalkers you control, and cant be sacrificed.
At the beginning of each opponents upkeep, you may have that player gain control of equipped creature until end of turn. If you do, untap it.
Equip {3}
*/
private static final String allSac = "Innocent Blood";
// Sorcery {B} Each player sacrifices a creature.
private static final String urchin = "Bile Urchin";
// Creature Spirit {B} Sacrifice Bile Urchin: Target player loses 1 life.
private static final String zombie = "Walking Corpse"; // 2/2 vanilla
private static final String vampire = "Barony Vampire"; // 3/2 vanilla
private static final String bairn = "Blood Bairn"; // Sacrifice another creature: ~ gets +2/+2 until EOT
private static final String jonIren = "Jon Irenicus, Shattered One";
// At the beginning of your end step, target opponent gains control of up to one target creature you control.
// Put two +1/+1 counters on it and tap it. Its goaded for the rest of the game and it gains This creature cant be sacrificed.
@Test
public void testAssaultSuitWithSacEffect() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4);
addCard(Zone.BATTLEFIELD, playerA, zombie);
addCard(Zone.BATTLEFIELD, playerA, assaultSuit);
addCard(Zone.BATTLEFIELD, playerB, vampire);
addCard(Zone.HAND, playerA, allSac);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", zombie);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, allSac);
setChoice(playerB, vampire); // to sacrifice (player A can't)
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, zombie, 1);
assertGraveyardCount(playerB, vampire, 1);
}
@Test
public void testAssaultSuitWithSacSourceCost() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3);
addCard(Zone.BATTLEFIELD, playerA, urchin);
addCard(Zone.BATTLEFIELD, playerA, assaultSuit);
checkPlayableAbility("Can sacrifice", 1, PhaseStep.UPKEEP, playerA, "Sacrifice ", true);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", urchin);
checkPlayableAbility("Can't sacrifice", 1, PhaseStep.BEGIN_COMBAT, playerA, "Sacrifice ", false);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAttachedTo(playerA, assaultSuit, urchin, true);
}
@Test
public void testAssaultSuitWithSacAnotherCost() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3);
addCard(Zone.BATTLEFIELD, playerA, zombie);
addCard(Zone.BATTLEFIELD, playerA, bairn);
addCard(Zone.BATTLEFIELD, playerA, assaultSuit);
checkPlayableAbility("Can sacrifice", 1, PhaseStep.UPKEEP, playerA, "Sacrifice another", true);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", zombie);
checkPlayableAbility("Can't sacrifice", 1, PhaseStep.BEGIN_COMBAT, playerA, "Sacrifice another", false);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAttachedTo(playerA, assaultSuit, zombie, true);
assertPowerToughness(playerA, bairn, 2, 2);
}
@Test
public void testJonIrenicusWithSacSourceCost() {
addCard(Zone.BATTLEFIELD, playerA, urchin);
addCard(Zone.BATTLEFIELD, playerA, jonIren);
checkPlayableAbility("Can sacrifice", 1, PhaseStep.UPKEEP, playerA, "Sacrifice ", true);
checkPlayableAbility("Can't sacrifice", 1, PhaseStep.UPKEEP, playerB, "Sacrifice ", false);
addTarget(playerA, playerB); // target opponent gains control
addTarget(playerA, urchin); // of up to one target creature you control
checkPlayableAbility("Can't sacrifice", 2, PhaseStep.UPKEEP, playerA, "Sacrifice ", false);
checkPlayableAbility("Can't sacrifice", 2, PhaseStep.UPKEEP, playerB, "Sacrifice ", false);
setStrictChooseMode(true);
setStopAt(2, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerB, urchin, 1);
assertCounterCount(urchin, CounterType.P1P1, 2);
}
}

View file

@ -38,7 +38,7 @@ public class MyrkulsEdictTest extends CardTestPlayerBase {
setDieRollResult(playerA, 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, myrkulsEdict);
addTarget(playerA, playerB);
addTarget(playerB, "Silvercoat Lion");
setChoice(playerB, "Silvercoat Lion");
execute();
@ -56,7 +56,7 @@ public class MyrkulsEdictTest extends CardTestPlayerBase {
setDieRollResult(playerA, 11);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, myrkulsEdict);
addTarget(playerB, "Silvercoat Lion");
setChoice(playerB, "Silvercoat Lion");
execute();
@ -76,7 +76,7 @@ public class MyrkulsEdictTest extends CardTestPlayerBase {
setDieRollResult(playerA, 20);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, myrkulsEdict);
addTarget(playerB, "Aesi, Tyrant of Gyre Strait");
setChoice(playerB, "Aesi, Tyrant of Gyre Strait");
execute();

View file

@ -31,7 +31,7 @@ public class WitchKingOfAngmarTest extends CardTestPlayerBase {
attack(2, playerB, watchwolf);
attack(2, playerB, swallower);
checkStackObject("Sacrifice trigger check", 2, PhaseStep.COMBAT_DAMAGE, playerB, "Whenever one or more creatures deal combat damage to you", 1);
addTarget(playerB, watchwolf); // choose which creature to sacrifice
setChoice(playerB, watchwolf); // choose which creature to sacrifice
runCode("check ring bear", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
Assert.assertNotNull(playerA.getRingBearer(game));
@ -67,4 +67,4 @@ public class WitchKingOfAngmarTest extends CardTestPlayerBase {
assertAbility(playerA, witchKing, IndestructibleAbility.getInstance(), false);
}
}
}

View file

@ -17,7 +17,7 @@ public class DaemogothTitanTest extends CardTestPlayerBaseWithAIHelps {
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
attack(1, playerA, "Daemogoth Titan");
addTarget(playerA, "Grizzly Bears");
setChoice(playerA, "Grizzly Bears");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);

View file

@ -21,7 +21,7 @@ public class DealsCombatDamageTriggerTest extends CardTestPlayerBase {
attack(1, playerA, drinker, playerB);
addTarget(playerA, memnite); // to sacrifice
setChoice(playerA, memnite); // to sacrifice
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
@ -56,8 +56,8 @@ public class DealsCombatDamageTriggerTest extends CardTestPlayerBase {
attack(1, playerA, drinker, playerB);
setChoice(playerA, "Whenever"); // order identical triggers
addTarget(playerA, drinker); // to sacrifice
addTarget(playerA, drinker); // to sacrifice
setChoice(playerA, drinker); // to sacrifice
setChoice(playerA, drinker); // to sacrifice
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);

View file

@ -29,7 +29,7 @@ public class OmnathLocusOfRageTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Diabolic Edict");
addTarget(playerB, playerA);
addTarget(playerA, "Omnath, Locus of Rage"); // sacrifice target
setChoice(playerA, "Omnath, Locus of Rage"); // sacrifice target
addTarget(playerA, playerB); // target for dies trigger with damage
setStrictChooseMode(true);

View file

@ -24,7 +24,7 @@ public class SilumgarScavengerTest extends CardTestPlayerBase {
// cast and exploit
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Silumgar Scavenger");
setChoice(playerA, true); // yes, exploit
addTarget(playerA, "Balduvian Bears");
setChoice(playerA, "Balduvian Bears");
checkPermanentCounters("boost", 1, PhaseStep.BEGIN_COMBAT, playerA, "Silumgar Scavenger", CounterType.P1P1, 1);
checkAbility("boost", 1, PhaseStep.BEGIN_COMBAT, playerA, "Silumgar Scavenger", HasteAbility.class, true);

View file

@ -77,7 +77,7 @@ public class CastCommanderTest extends CardTestCommanderDuelBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Patron of the Orochi");
setChoice(playerA, true);
addTarget(playerA, "Coiled Tinviper");
setChoice(playerA, "Coiled Tinviper");
setStrictChooseMode(true);