diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SuspendTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SuspendTest.java index 8e6f75590ff..5f699c9382d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SuspendTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SuspendTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.abilities.keywords; import mage.abilities.keyword.HasteAbility; @@ -9,15 +8,13 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * - * @author LevelX2 + * @author LevelX2, JayDi85 */ public class SuspendTest extends CardTestPlayerBase { /** * Tests Epochrasite works (give suspend to a exiled card) When Epochrasite * dies, exile it with three time counters on it and it gains suspend. - * */ @Test public void testEpochrasite() { @@ -46,7 +43,6 @@ public class SuspendTest extends CardTestPlayerBase { * Tests Jhoira of the Ghitu works (give suspend to a exiled card) {2}, * Exile a nonland card from your hand: Put four time counters on the exiled * card. If it doesn't have suspend, it gains suspend. - * */ @Test public void testJhoiraOfTheGhitu() { @@ -71,7 +67,6 @@ public class SuspendTest extends CardTestPlayerBase { /** * Tests that a spell countered with delay goes to exile with 3 time * counters and can be cast after the 3 counters are removed - * */ @Test public void testDelay() { @@ -141,7 +136,6 @@ public class SuspendTest extends CardTestPlayerBase { /** * Suppression Field incorrectly makes suspend cards cost 2 more to suspend. * It made my Rift Bolt cost 2R to suspend instead of R - * */ @Test public void testCostManipulation() { @@ -164,9 +158,8 @@ public class SuspendTest extends CardTestPlayerBase { * Cards cast from other zones that aren't the hand should not trigger * Knowledge Pool, as it states that only cards cast from the hand should be * exiled afterwards. - * + *

* Example: cards coming off suspend shouldn't trigger Knowledge Pool. - * */ @Test public void testThatNotCastFromHand() { @@ -199,4 +192,124 @@ public class SuspendTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Silvercoat Lion", 0); } + + /* + Delay {1}{U} + + Counter target spell. If the spell is countered this way, exile it with three time counters on it instead of putting + it into its owner’s graveyard. If it doesn’t have suspend, it gains suspend. (At the beginning of its owner’s upkeep, + remove a time counter from that card. When the last is removed, the player plays it without paying its mana cost. + If it’s a creature, it has haste.) + + Bug: Casting Delay on a fused Wear // Tear resulted in time counters never coming off it. It just sat there with + three counters every turn. See https://github.com/magefree/mage/issues/6549 + */ + + @Test + public void test_Delay_SimpleSpell() { + // + addCard(Zone.HAND, playerA, "Delay", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + // + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + // cast spell and counter it with delay + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Delay", "Lightning Bolt", "Lightning Bolt"); + // + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkLife("after counter", 1, PhaseStep.PRECOMBAT_MAIN, playerB, 20); + checkExileCount("after counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", 1); + + // 3 time counters removes on upkeep (3, 5, 7) and cast again + setChoice(playerA, "Cast"); + addTarget(playerA, playerB); + checkLife("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerB, 20 - 3); + checkGraveyardCount("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", 1); + + setStrictChooseMode(true); + setStopAt(7, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_Delay_SplitSingle() { + addCard(Zone.HAND, playerA, "Delay", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + // + // Wear {1}{R} Destroy target artifact. + // Tear {W} Destroy target enchantment. + addCard(Zone.HAND, playerA, "Wear // Tear", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + // + addCard(Zone.BATTLEFIELD, playerB, "Bident of Thassa", 1); // Legendary Enchantment Artifact + addCard(Zone.BATTLEFIELD, playerB, "Bow of Nylea", 1); // Legendary Enchantment Artifact + + // cast spell and counter it with delay + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wear", "Bident of Thassa"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Delay", "Wear", "Wear"); + // + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after counter", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Bident of Thassa", 1); + checkPermanentCount("after counter", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Bow of Nylea", 1); + checkExileCount("after counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wear // Tear", 1); + + // 3 time counters removes on upkeep (3, 5, 7) and cast again + setChoice(playerA, "Cast Wear"); + addTarget(playerA, "Bident of Thassa"); + checkPermanentCount("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerB, "Bident of Thassa", 0); + checkPermanentCount("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerB, "Bow of Nylea", 1); + checkGraveyardCount("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerA, "Wear // Tear", 1); + + setStrictChooseMode(true); + setStopAt(7, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_Delay_SplitFused() { + /* + Bug: Casting Delay on a fused Wear // Tear resulted in time counters never coming off it. It just sat there with + three counters every turn. See https://github.com/magefree/mage/issues/6549 + */ + + // + addCard(Zone.HAND, playerA, "Delay", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + // + // Wear {1}{R} Destroy target artifact. + // Tear {W} Destroy target enchantment. + addCard(Zone.HAND, playerA, "Wear // Tear", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + // + addCard(Zone.BATTLEFIELD, playerB, "Bident of Thassa", 1); // Legendary Enchantment Artifact + addCard(Zone.BATTLEFIELD, playerB, "Bow of Nylea", 1); // Legendary Enchantment Artifact + + // cast fused spell and counter it with delay + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "fused Wear // Tear"); + addTarget(playerA, "Bident of Thassa"); + addTarget(playerA, "Bow of Nylea"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Delay", "Cast fused Wear // Tear", "Cast fused Wear // Tear"); + // + checkPermanentCount("after counter", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Bident of Thassa", 1); + checkPermanentCount("after counter", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Bow of Nylea", 1); + checkExileCount("after counter", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Wear // Tear", 1); + + // 3 time counters removes on upkeep (3, 5, 7) and cast again (fused cards can't be played from exile zone, so select split spell only) + setChoice(playerA, "Cast Wear"); + addTarget(playerA, "Bident of Thassa"); + checkPermanentCount("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerB, "Bident of Thassa", 0); + checkPermanentCount("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerB, "Bow of Nylea", 1); + checkGraveyardCount("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerA, "Wear // Tear", 1); + + setStrictChooseMode(true); + setStopAt(7, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } } diff --git a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java index 2bca55f26d3..3b066bf316a 100644 --- a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java @@ -1,8 +1,5 @@ package mage.abilities.keyword; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; import mage.MageObject; import mage.MageObjectReference; import mage.abilities.Ability; @@ -27,10 +24,13 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.targetpointer.FixedTarget; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + /** - * * 502.59. Suspend - * + *

* 502.59a Suspend is a keyword that represents three abilities. The first is a * static ability that functions while the card with suspend is in a player's * hand. The second and third are triggered abilities that function in the @@ -42,13 +42,13 @@ import mage.target.targetpointer.FixedTarget; * card, if it's removed from the game, play it without paying its mana cost if * able. If you can't, it remains removed from the game. If you play it this way * and it's a creature, it gains haste until you lose control of it." - * + *

* 502.59b A card is "suspended" if it's in the removed-from-the-game zone, has * suspend, and has a time counter on it. - * + *

* 502.59c Playing a spell as an effect of its suspend ability follows the rules * for paying alternative costs in rules 409.1b and 409.1f-h. - * + *

* The phrase "if you could play this card from your hand" checks only for * timing restrictions and permissions. This includes both what's inherent in * the card's type (for example, if the card with suspend is a creature, it must @@ -57,38 +57,38 @@ import mage.target.targetpointer.FixedTarget; * actually follow all steps in playing the card is irrelevant. If the card is * impossible to play due to a lack of legal targets or an unpayable mana cost, * for example, it may still be removed from the game with suspend. - * + *

* Removing a card from the game with its suspend ability is not playing that * card. This action doesn't use the stack and can't be responded to. - * + *

* If a spell with suspend has targets, the targets are chosen when the spell is * played, not when it's removed from the game. - * + *

* If the first triggered ability of suspend is countered, no time counter is * removed. The ability will trigger again during its owner's next upkeep. - * + *

* When the last time counter is removed from a suspended card, the second * triggered ability of suspend will trigger. It doesn't matter why the time * counter was removed or whose effect removed it. (The _Time Spiral_ reminder * text is misleading on this point.) - * + *

* If the second triggered ability of suspend is countered, the card can't be * played. It remains in the removed-from-the-game zone without any time * counters on it for the rest of the game, and it's no longer considered * suspended. - * + *

* If the second triggered ability of suspend resolves, the card's owner must * play the spell if possible, even if that player doesn't want to. Normal * timing considerations for the spell are ignored (for example, if the * suspended card is a creature and this ability resolves during your upkeep, * you're able to play the card), but other play restrictions are not ignored. - * + *

* If the second triggered ability of suspend resolves and the suspended card * can't be played due to a lack of legal targets or a play restriction, for * example, it remains in the removed-from-the-game zone without any time * counters on it for the rest of the game, and it's no longer considered * suspended. - * + *

* As the second triggered ability of suspend resolves, if playing the suspended * card involves an additional cost, the card's owner must pay that cost if * able. If they can't, the card remains removed from the game. If the @@ -99,14 +99,12 @@ import mage.target.targetpointer.FixedTarget; * cost, then they have a choice: The player may play the spell, produce mana, * and pay the cost. Or the player may choose to play no mana abilities, thus * making the card impossible to play because the additional mana can't be paid. - * + *

* A creature played via suspend comes into play with haste. It still has haste * after the first turn it's in play as long as the same player controls it. As * soon as another player takes control of it, it loses haste. * - * * @author LevelX2 - * */ public class SuspendAbility extends SpecialAction { @@ -117,9 +115,9 @@ public class SuspendAbility extends SpecialAction { * Gives the card the SuspendAbility * * @param suspend - amount of time counters, if Integer.MAX_VALUE is set - * there will be {X} costs and X counters added - * @param cost - null is used for temporary gained suspend ability - * @param card - card that has the suspend ability + * there will be {X} costs and X counters added + * @param cost - null is used for temporary gained suspend ability + * @param card - card that has the suspend ability */ public SuspendAbility(int suspend, ManaCost cost, Card card) { this(suspend, cost, card, false); @@ -138,13 +136,13 @@ public class SuspendAbility extends SpecialAction { } StringBuilder sb = new StringBuilder("Suspend "); if (cost != null) { - sb.append(suspend == Integer.MAX_VALUE ? "X" : suspend).append("—").append(cost.getText()).append(suspend + sb.append(suspend == Integer.MAX_VALUE ? "X" : suspend).append("—").append(cost.getText()).append(suspend == Integer.MAX_VALUE ? ". X can't be 0" : ""); if (!shortRule) { sb.append(" (Rather than cast this card from your hand, pay ") .append(cost.getText()) .append(" and exile it with ") - .append((suspend == 1 ? "a time counter" : (suspend == Integer.MAX_VALUE + .append((suspend == 1 ? "a time counter" : (suspend == Integer.MAX_VALUE ? "X time counters" : suspend + " time counters"))) .append(" on it.") .append(" At the beginning of your upkeep, remove a time counter. " @@ -176,7 +174,7 @@ public class SuspendAbility extends SpecialAction { ability.setControllerId(card.getOwnerId()); game.getState().addOtherAbility(card, ability); - SuspendBeginningOfUpkeepInterveningIfTriggeredAbility ability1 = + SuspendBeginningOfUpkeepInterveningIfTriggeredAbility ability1 = new SuspendBeginningOfUpkeepInterveningIfTriggeredAbility(); ability1.setSourceId(card.getId()); ability1.setControllerId(card.getOwnerId()); @@ -214,8 +212,8 @@ public class SuspendAbility extends SpecialAction { MageObject object = game.getObject(sourceId); return new ActivationStatus(object.isInstant() || object.hasAbility(FlashAbility.getInstance(), game) - || null != game.getContinuousEffects().asThough(sourceId, - AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game) + || null != game.getContinuousEffects().asThough(sourceId, + AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game) || game.canPlaySorcery(playerId), null); } @@ -241,7 +239,7 @@ class SuspendExileEffect extends OneShotEffect { public SuspendExileEffect(int suspend) { super(Outcome.PutCardInPlay); - this.staticText = new StringBuilder("Suspend ").append(suspend + this.staticText = new StringBuilder("Suspend ").append(suspend == Integer.MAX_VALUE ? "X" : suspend).toString(); this.suspend = suspend; } @@ -262,7 +260,7 @@ class SuspendExileEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); if (card != null && controller != null) { UUID exileId = SuspendAbility.getSuspendExileId(controller.getId(), game); - if (controller.moveCardToExileWithInfo(card, exileId, "Suspended cards of " + if (controller.moveCardToExileWithInfo(card, exileId, "Suspended cards of " + controller.getName(), source.getSourceId(), game, Zone.HAND, true)) { if (suspend == Integer.MAX_VALUE) { suspend = source.getManaCostsToPay().getX(); @@ -298,11 +296,9 @@ class SuspendPlayCardAbility extends TriggeredAbilityImpl { public boolean checkTrigger(GameEvent event, Game game) { if (event.getTargetId().equals(getSourceId())) { Card card = game.getCard(getSourceId()); - if (card != null + return card != null && game.getState().getZone(card.getId()) == Zone.EXILED - && card.getCounters(game).getCount(CounterType.TIME) == 0) { - return true; - } + && card.getCounters(game).getCount(CounterType.TIME) == 0; } return false; } @@ -340,8 +336,9 @@ class SuspendPlayCardEffect extends OneShotEffect { Card card = game.getCard(source.getSourceId()); if (player != null && card != null) { // remove temporary suspend ability (used e.g. for Epochrasite) + // TODO: isGainedTemporary is not set or use in other places, so it can be deleted?! List abilitiesToRemove = new ArrayList<>(); - for (Ability ability : card.getAbilities()) { + for (Ability ability : card.getAbilities(game)) { if (ability instanceof SuspendAbility) { if (((SuspendAbility) ability).isGainedTemporary()) { abilitiesToRemove.add(ability); @@ -350,7 +347,7 @@ class SuspendPlayCardEffect extends OneShotEffect { } if (!abilitiesToRemove.isEmpty()) { for (Ability ability : card.getAbilities()) { - if (ability instanceof SuspendBeginningOfUpkeepInterveningIfTriggeredAbility + if (ability instanceof SuspendBeginningOfUpkeepInterveningIfTriggeredAbility || ability instanceof SuspendPlayCardAbility) { abilitiesToRemove.add(ability); } @@ -416,7 +413,7 @@ class GainHasteEffect extends ContinuousEffectImpl { } return true; } - if (game.getState().getZoneChangeCounter(((FixedTarget) getTargetPointer()).getTarget()) + if (game.getState().getZoneChangeCounter(((FixedTarget) getTargetPointer()).getTarget()) >= ((FixedTarget) getTargetPointer()).getZoneChangeCounter()) { this.discard(); } @@ -428,8 +425,8 @@ class GainHasteEffect extends ContinuousEffectImpl { class SuspendBeginningOfUpkeepInterveningIfTriggeredAbility extends ConditionalInterveningIfTriggeredAbility { public SuspendBeginningOfUpkeepInterveningIfTriggeredAbility() { - super(new BeginningOfUpkeepTriggeredAbility(Zone.EXILED, new RemoveCounterSourceEffect(CounterType.TIME.createInstance()), - TargetController.YOU, false), + super(new BeginningOfUpkeepTriggeredAbility(Zone.EXILED, new RemoveCounterSourceEffect(CounterType.TIME.createInstance()), + TargetController.YOU, false), SuspendedCondition.instance, "At the beginning of your upkeep, if this card ({this}) is suspended, remove a time counter from it."); this.setRuleVisible(false); diff --git a/Mage/src/main/java/mage/cards/SplitCard.java b/Mage/src/main/java/mage/cards/SplitCard.java index 21c58c6c13c..6845daa0401 100644 --- a/Mage/src/main/java/mage/cards/SplitCard.java +++ b/Mage/src/main/java/mage/cards/SplitCard.java @@ -1,8 +1,5 @@ package mage.cards; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; import mage.MageObject; import mage.abilities.Abilities; import mage.abilities.AbilitiesImpl; @@ -13,6 +10,10 @@ import mage.constants.SpellAbilityType; import mage.constants.Zone; import mage.game.Game; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + /** * @author LevelX2 */ @@ -135,11 +136,13 @@ public abstract class SplitCard extends CardImpl { public Abilities getAbilities(Game game) { Abilities allAbilites = new AbilitiesImpl<>(); for (Ability ability : super.getAbilities(game)) { + // ignore split abilities TODO: why it here, for GUI's cleanup in card texts? Maybe it can be removed if (ability instanceof SpellAbility - && ((SpellAbility) ability).getSpellAbilityType() != SpellAbilityType.SPLIT - && ((SpellAbility) ability).getSpellAbilityType() != SpellAbilityType.SPLIT_AFTERMATH) { - allAbilites.add(ability); + && (((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.SPLIT + || ((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.SPLIT_AFTERMATH)) { + continue; } + allAbilites.add(ability); } allAbilites.addAll(leftHalfCard.getAbilities(game)); allAbilites.addAll(rightHalfCard.getAbilities(game));