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