diff --git a/Mage.Sets/src/mage/cards/f/FeatherTheRedeemed.java b/Mage.Sets/src/mage/cards/f/FeatherTheRedeemed.java index fcc6e8e1b54..47cc4950e48 100644 --- a/Mage.Sets/src/mage/cards/f/FeatherTheRedeemed.java +++ b/Mage.Sets/src/mage/cards/f/FeatherTheRedeemed.java @@ -95,7 +95,7 @@ class FeatherTheRedeemedTriggeredAbility extends TriggeredAbilityImpl { if (permanent != null && permanent.isCreature(game) && permanent.isControlledBy(getControllerId())) { this.getEffects().clear(); - this.addEffect(new FeatherTheRedeemedEffect(new MageObjectReference(spell.getCard(), game))); + this.addEffect(new FeatherTheRedeemedEffect(spell, game)); return true; } } @@ -106,7 +106,7 @@ class FeatherTheRedeemedTriggeredAbility extends TriggeredAbilityImpl { if (permanent != null && permanent.isCreature(game) && permanent.isControlledBy(getControllerId())) { this.getEffects().clear(); - this.addEffect(new FeatherTheRedeemedEffect(new MageObjectReference(spell.getCard(), game))); + this.addEffect(new FeatherTheRedeemedEffect(spell, game)); return true; } } @@ -125,21 +125,25 @@ class FeatherTheRedeemedTriggeredAbility extends TriggeredAbilityImpl { class FeatherTheRedeemedEffect extends ReplacementEffectImpl { - private final MageObjectReference mor; - - FeatherTheRedeemedEffect(MageObjectReference mor) { + // we store both Spell and Card to work properly on split cards. + private final MageObjectReference morSpell; + private final MageObjectReference morCard; + + FeatherTheRedeemedEffect(Spell spell, Game game) { super(Duration.OneUse, Outcome.Benefit); - this.mor = mor; + this.morSpell = new MageObjectReference(spell.getCard(), game); + this.morCard = new MageObjectReference(spell.getMainCard(), game); } private FeatherTheRedeemedEffect(final FeatherTheRedeemedEffect effect) { super(effect); - this.mor = effect.mor; + this.morSpell = effect.morSpell; + this.morCard = effect.morCard; } @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { - Spell sourceSpell = game.getStack().getSpell(event.getTargetId()); + Spell sourceSpell = morSpell.getSpell(game); if (sourceSpell == null || sourceSpell.isCopy()) { return false; } @@ -162,15 +166,11 @@ class FeatherTheRedeemedEffect extends ReplacementEffectImpl { @Override public boolean applies(GameEvent event, Ability source, Game game) { ZoneChangeEvent zEvent = ((ZoneChangeEvent) event); - if (zEvent.getFromZone() != Zone.STACK - || zEvent.getToZone() != Zone.GRAVEYARD - || event.getSourceId() == null - || !event.getSourceId().equals(event.getTargetId()) - || !mor.equals(new MageObjectReference(event.getTargetId(), game))) { - return false; - } - return true; - } + return Zone.STACK.equals(zEvent.getFromZone()) + && Zone.GRAVEYARD.equals(zEvent.getToZone()) + && morSpell.refersTo(event.getSourceId(), game) // this is how we check that the spell resolved properly (and was not countered or the like) + && morCard.refersTo(event.getTargetId(), game); // this is how we check that the card being moved is the one we want. + } @Override public FeatherTheRedeemedEffect copy() { diff --git a/Mage.Sets/src/mage/cards/g/GandalfOfTheSecretFire.java b/Mage.Sets/src/mage/cards/g/GandalfOfTheSecretFire.java index 48b78922b53..8e11db87318 100644 --- a/Mage.Sets/src/mage/cards/g/GandalfOfTheSecretFire.java +++ b/Mage.Sets/src/mage/cards/g/GandalfOfTheSecretFire.java @@ -87,16 +87,20 @@ class GandalfOfTheSecretFireTriggeredAbility extends TriggeredAbilityImpl { class GandalfOfTheSecretFireEffect extends ReplacementEffectImpl { - private final MageObjectReference mor; + // we store both Spell and Card to work properly on split cards. + private final MageObjectReference morSpell; + private final MageObjectReference morCard; GandalfOfTheSecretFireEffect(Spell spell, Game game) { super(Duration.OneUse, Outcome.Benefit); - this.mor = new MageObjectReference(spell.getCard(), game); + this.morSpell = new MageObjectReference(spell.getCard(), game); + this.morCard = new MageObjectReference(spell.getMainCard(), game); } private GandalfOfTheSecretFireEffect(final GandalfOfTheSecretFireEffect effect) { super(effect); - this.mor = effect.mor; + this.morSpell = effect.morSpell; + this.morCard = effect.morCard; } @Override @@ -107,7 +111,7 @@ class GandalfOfTheSecretFireEffect extends ReplacementEffectImpl { @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { Player controller = game.getPlayer(source.getControllerId()); - Spell sourceSpell = game.getStack().getSpell(event.getTargetId()); + Spell sourceSpell = morSpell.getSpell(game); if (controller == null || sourceSpell == null || sourceSpell.isCopy()) { return false; } @@ -124,14 +128,9 @@ class GandalfOfTheSecretFireEffect extends ReplacementEffectImpl { @Override public boolean applies(GameEvent event, Ability source, Game game) { ZoneChangeEvent zEvent = ((ZoneChangeEvent) event); - if (zEvent.getFromZone() != Zone.STACK - || zEvent.getToZone() != Zone.GRAVEYARD - || event.getSourceId() == null - || !event.getSourceId().equals(event.getTargetId()) - || !mor.equals(new MageObjectReference(event.getTargetId(), game))) { - return false; - } - Spell spell = game.getStack().getSpell(mor.getSourceId()); - return spell != null && spell.isInstantOrSorcery(game); + return Zone.STACK.equals(zEvent.getFromZone()) + && Zone.GRAVEYARD.equals(zEvent.getToZone()) + && morSpell.refersTo(event.getSourceId(), game) // this is how we check that the spell resolved properly (and was not countered or the like) + && morCard.refersTo(event.getTargetId(), game); // this is how we check that the card being moved is the one we want. } } diff --git a/Mage.Sets/src/mage/cards/r/RodOfAbsorption.java b/Mage.Sets/src/mage/cards/r/RodOfAbsorption.java index e3c2ff36421..3bcfa2265b1 100644 --- a/Mage.Sets/src/mage/cards/r/RodOfAbsorption.java +++ b/Mage.Sets/src/mage/cards/r/RodOfAbsorption.java @@ -93,16 +93,20 @@ class RodOfAbsorptionTriggeredAbility extends TriggeredAbilityImpl { class RodOfAbsorptionExileEffect extends ReplacementEffectImpl { - private final MageObjectReference mor; + // we store both Spell and Card to work properly on split cards. + private final MageObjectReference morSpell; + private final MageObjectReference morCard; RodOfAbsorptionExileEffect(Spell spell, Game game) { super(Duration.WhileOnStack, Outcome.Benefit); - this.mor = new MageObjectReference(spell, game); + this.morSpell = new MageObjectReference(spell.getCard(), game); + this.morCard = new MageObjectReference(spell.getMainCard(), game); } private RodOfAbsorptionExileEffect(final RodOfAbsorptionExileEffect effect) { super(effect); - this.mor = effect.mor; + this.morSpell = effect.morSpell; + this.morCard = effect.morCard; } @Override @@ -131,15 +135,10 @@ class RodOfAbsorptionExileEffect extends ReplacementEffectImpl { @Override public boolean applies(GameEvent event, Ability source, Game game) { ZoneChangeEvent zEvent = ((ZoneChangeEvent) event); - if (zEvent.getFromZone() != Zone.STACK - || zEvent.getToZone() != Zone.GRAVEYARD - || event.getSourceId() == null - || !event.getSourceId().equals(event.getTargetId()) - || mor.getZoneChangeCounter() != game.getState().getZoneChangeCounter(event.getSourceId())) { - return false; - } - Spell spell = game.getStack().getSpell(mor.getSourceId()); - return spell != null && spell.isInstantOrSorcery(game); + return Zone.STACK.equals(zEvent.getFromZone()) + && Zone.GRAVEYARD.equals(zEvent.getToZone()) + && morSpell.refersTo(event.getSourceId(), game) // this is how we check that the spell resolved properly (and was not countered or the like) + && morCard.refersTo(event.getTargetId(), game); // this is how we check that the card being moved is the one we want. } @Override diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/FeatherTheRedeemedTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/FeatherTheRedeemedTest.java index 02b014f3a0d..0aed0945d09 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/FeatherTheRedeemedTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/FeatherTheRedeemedTest.java @@ -115,4 +115,47 @@ public class FeatherTheRedeemedTest extends CardTestPlayerBase { setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); execute(); } + + @Test + public void test_SplitCard() { + // cast fire, put to exile, return to hand + addCard(Zone.BATTLEFIELD, playerA, "Feather, the Redeemed"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.HAND, playerA, "Fire // Ice", 1); + + // cast and put to exile + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Fire"); + addTargetAmount(playerA, "Feather, the Redeemed", 2); + checkExileCount("turn 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Fire // Ice", 1); + checkHandCardCount("turn 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Fire // Ice", 0); + + // return to hand at the next end step + checkExileCount("turn 1 after", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Fire // Ice", 0); + checkHandCardCount("turn 1 after", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Fire // Ice", 1); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + } + + @Test + public void test_Adventure() { + // cast stomp, put to exile, no return to hand + addCard(Zone.BATTLEFIELD, playerA, "Feather, the Redeemed"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.HAND, playerA, "Bonecrusher Giant", 1); + + // cast and put to exile + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Stomp", "Feather, the Redeemed"); + checkExileCount("turn 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Bonecrusher Giant", 1); + checkHandCardCount("turn 1", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Bonecrusher Giant", 0); + + // no return to hand, as it does not go to graveyard on resolve + checkExileCount("turn 1 after", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Bonecrusher Giant", 1); + checkHandCardCount("turn 1 after", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Bonecrusher Giant", 0); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ltc/GandalfOfTheSecretFireTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltc/GandalfOfTheSecretFireTest.java new file mode 100644 index 00000000000..e082e07831e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltc/GandalfOfTheSecretFireTest.java @@ -0,0 +1,161 @@ +package org.mage.test.cards.single.ltc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class GandalfOfTheSecretFireTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.g.GandalfOfTheSecretFire Gandalf of the Secret Fire} {1}{U}{R}{W} + * Legendary Creature — Avatar Wizard + * Whenever you cast an instant or sorcery spell from your hand during an opponent’s turn, exile that card with three time counters on it instead of putting it into your graveyard as it resolves. Then if the exiled card doesn’t have suspend, it gains suspend. (At the beginning of your upkeep, remove a time counter. When the last is removed, you may play it without paying its mana cost.) + * 3/4 + */ + private static final String gandalf = "Gandalf of the Secret Fire"; + + @Test + public void test_yourturn() { + addCard(Zone.BATTLEFIELD, playerA, gandalf, 1); + + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 3); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + } + + @Test + public void test_oppturn_suspend() { + addCard(Zone.BATTLEFIELD, playerA, gandalf, 1); + + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + + checkExileCount("1: bolt in exile", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", 1); + checkCardCounters("1: bolt has 3 time counters", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", CounterType.TIME, 3); + + // turn 3: from 3 to 2 time counter + checkCardCounters("2: bolt has 2 time counters", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", CounterType.TIME, 2); + + // turn 5: from 2 to 1 time counter + checkCardCounters("3: bolt has 1 time counters", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", CounterType.TIME, 1); + + setChoice(playerA, true); // yes to cast from suspend removing last counter + addTarget(playerA, playerB); + + setStrictChooseMode(true); + setStopAt(7, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 6); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + } + + @Test + public void test_split_suspend() { + addCard(Zone.BATTLEFIELD, playerA, gandalf, 1); + + addCard(Zone.HAND, playerA, "Fire // Ice", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Fire", playerB); + addTargetAmount(playerA, playerB, 2); + + checkExileCount("1: fire//ice in exile", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, "Fire // Ice", 1); + checkCardCounters("1: fire//ice has 3 time counters", 2, PhaseStep.POSTCOMBAT_MAIN, playerA, "Fire // Ice", CounterType.TIME, 3); + + // turn 3: from 3 to 2 time counter + checkCardCounters("2: fire//ice has 2 time counters", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Fire // Ice", CounterType.TIME, 2); + + // turn 5: from 2 to 1 time counter + checkCardCounters("3: fire//ice has 1 time counters", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Fire // Ice", CounterType.TIME, 1); + + setChoice(playerA, true); // yes to cast from suspend removing last counter + setChoice(playerA, "Cast Fire"); // choose to cast Fire side + addTargetAmount(playerA, playerB, 2); + + setStrictChooseMode(true); + setStopAt(7, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 4); + assertGraveyardCount(playerA, "Fire // Ice", 1); + } + + @Test + public void test_oppturn_counterspell_suspend() { + addCard(Zone.BATTLEFIELD, playerA, gandalf, 1); + + addCard(Zone.HAND, playerA, "Counterspell", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + + addCard(Zone.HAND, playerB, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerB); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Counterspell", "Lightning Bolt"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 20); + assertExileCount(playerA, "Counterspell", 1); + assertGraveyardCount(playerB, "Lightning Bolt", 1); + } + + @Test + public void test_oppturn_countered_nosuspend() { + addCard(Zone.BATTLEFIELD, playerB, gandalf, 1); + + addCard(Zone.HAND, playerA, "Counterspell", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + + addCard(Zone.HAND, playerB, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", playerB); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Counterspell", "Lightning Bolt", "Lightning Bolt"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Counterspell", 1); + assertGraveyardCount(playerB, "Lightning Bolt", 1); + } + + @Test + public void test_adventure_nosuspend() { + addCard(Zone.BATTLEFIELD, playerA, gandalf, 1); + + addCard(Zone.HAND, playerA, "Bonecrusher Giant", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Stomp", playerB); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 2); + assertExileCount(playerA, "Bonecrusher Giant", 1); + // since an Adventure card is not going to the graveyard on resolve, Gandalf's trigger does not suspend it. + assertCounterOnExiledCardCount("Bonecrusher Giant", CounterType.TIME, 0); + } +}