diff --git a/Mage.Sets/src/mage/cards/a/ArcaneBombardment.java b/Mage.Sets/src/mage/cards/a/ArcaneBombardment.java index b20f51a8923..32d921ff7c5 100644 --- a/Mage.Sets/src/mage/cards/a/ArcaneBombardment.java +++ b/Mage.Sets/src/mage/cards/a/ArcaneBombardment.java @@ -99,8 +99,6 @@ class ArcaneBombardmentEffect extends OneShotEffect { Cards copies = new CardsImpl(); for (Card card : exileZone.getCards(game)) { Card copiedCard = game.copyCard(card, source, source.getControllerId()); - game.getExile().add(source.getSourceId(), "", copiedCard); - game.getState().setZone(copiedCard.getId(), Zone.EXILED); copies.add(copiedCard); } for (Card copiedCard : copies.getCards(game)) { diff --git a/Mage.Sets/src/mage/cards/e/EliteArcanist.java b/Mage.Sets/src/mage/cards/e/EliteArcanist.java index 1abd9544b56..03f5f0ecee1 100644 --- a/Mage.Sets/src/mage/cards/e/EliteArcanist.java +++ b/Mage.Sets/src/mage/cards/e/EliteArcanist.java @@ -160,8 +160,6 @@ class EliteArcanistCopyEffect extends OneShotEffect { if (controller != null) { Card copiedCard = game.copyCard(imprintedInstant, source, source.getControllerId()); if (copiedCard != null) { - game.getExile().add(source.getSourceId(), "", copiedCard); - game.getState().setZone(copiedCard.getId(), Zone.EXILED); if (controller.chooseUse(Outcome.PlayForFree, "Cast the copied card without paying mana cost?", source, game)) { game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); Boolean cardWasCast = controller.cast(controller.chooseAbilityForCast(copiedCard, game, true), diff --git a/Mage.Sets/src/mage/cards/g/GodEternalKefnet.java b/Mage.Sets/src/mage/cards/g/GodEternalKefnet.java index 0cf9aed1b64..3a6b6c114e7 100644 --- a/Mage.Sets/src/mage/cards/g/GodEternalKefnet.java +++ b/Mage.Sets/src/mage/cards/g/GodEternalKefnet.java @@ -103,7 +103,6 @@ class GodEternalKefnetDrawCardReplacementEffect extends ReplacementEffectImpl { blueprint.addAbility(new SimpleStaticAbility(Zone.ALL, new SpellCostReductionSourceEffect(2))); } Card copiedCard = game.copyCard(blueprint, source, source.getControllerId()); - you.moveCardToHandWithInfo(copiedCard, source, game, true); // The copy is created in and cast from your hand. (2019-05-03) game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); you.cast(you.chooseAbilityForCast(copiedCard, game, false), game, false, new ApprovingObject(source, game)); game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), null); diff --git a/Mage.Sets/src/mage/cards/i/IsochronScepter.java b/Mage.Sets/src/mage/cards/i/IsochronScepter.java index 1d8f5ad4467..bf247e63ca6 100644 --- a/Mage.Sets/src/mage/cards/i/IsochronScepter.java +++ b/Mage.Sets/src/mage/cards/i/IsochronScepter.java @@ -139,8 +139,6 @@ class IsochronScepterCopyEffect extends OneShotEffect { if (controller.chooseUse(outcome, "Create a copy of " + imprintedInstant.getName() + '?', source, game)) { Card copiedCard = game.copyCard(imprintedInstant, source, source.getControllerId()); if (copiedCard != null) { - game.getExile().add(source.getSourceId(), "", copiedCard); - game.getState().setZone(copiedCard.getId(), Zone.EXILED); if (controller.chooseUse(outcome, "Cast the copied card without paying mana cost?", source, game)) { if (copiedCard.getSpellAbility() != null) { game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); diff --git a/Mage.Sets/src/mage/cards/j/JujuBubble.java b/Mage.Sets/src/mage/cards/j/JujuBubble.java index 6fec374ad9d..2e45c1b4c15 100644 --- a/Mage.Sets/src/mage/cards/j/JujuBubble.java +++ b/Mage.Sets/src/mage/cards/j/JujuBubble.java @@ -1,7 +1,7 @@ package mage.cards.j; import java.util.UUID; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.PlayCardTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.ManaCostsImpl; @@ -11,9 +11,8 @@ import mage.abilities.keyword.CumulativeUpkeepAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.TargetController; import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; /** * @@ -28,7 +27,8 @@ public final class JujuBubble extends CardImpl { this.addAbility(new CumulativeUpkeepAbility(new ManaCostsImpl<>("{1}"))); // When you play a card, sacrifice Juju Bubble. - this.addAbility(new JujuBubbleTriggeredAbility()); + this.addAbility(new PlayCardTriggeredAbility(TargetController.YOU, Zone.BATTLEFIELD, + new SacrificeSourceEffect(), false)); // {2}: You gain 1 life. this.addAbility(new SimpleActivatedAbility(new GainLifeEffect(1), new GenericManaCost(1))); @@ -42,35 +42,4 @@ public final class JujuBubble extends CardImpl { public JujuBubble copy() { return new JujuBubble(this); } -} - -class JujuBubbleTriggeredAbility extends TriggeredAbilityImpl { - - JujuBubbleTriggeredAbility() { - super(Zone.BATTLEFIELD, new SacrificeSourceEffect(), false); - } - - JujuBubbleTriggeredAbility(final JujuBubbleTriggeredAbility ability) { - super(ability); - } - - @Override - public JujuBubbleTriggeredAbility copy() { - return new JujuBubbleTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.SPELL_CAST || event.getType() == GameEvent.EventType.LAND_PLAYED; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - return event.getPlayerId().equals(this.getControllerId()); - } - - @Override - public String getRule() { - return "When you play a card, sacrifice {this}"; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/m/MizzixsMastery.java b/Mage.Sets/src/mage/cards/m/MizzixsMastery.java index ba8b0075d8d..2abb9ecb215 100644 --- a/Mage.Sets/src/mage/cards/m/MizzixsMastery.java +++ b/Mage.Sets/src/mage/cards/m/MizzixsMastery.java @@ -89,7 +89,7 @@ class MizzixsMasteryOverloadEffect extends OneShotEffect { while (controller.canRespond() && continueCasting && !copiedCards.isEmpty()) { - TargetCard targetCard = new TargetCard(0, 1, Zone.OUTSIDE, + TargetCard targetCard = new TargetCard(0, 1, Zone.EXILED, new FilterCard("copied card to cast without paying its mana cost?")); targetCard.setNotTarget(true); if (controller.chooseTarget(Outcome.PlayForFree, copiedCards, targetCard, source, game)) { diff --git a/Mage.Sets/src/mage/cards/m/MnemonicDeluge.java b/Mage.Sets/src/mage/cards/m/MnemonicDeluge.java index cc21aed30a6..921297ab89e 100644 --- a/Mage.Sets/src/mage/cards/m/MnemonicDeluge.java +++ b/Mage.Sets/src/mage/cards/m/MnemonicDeluge.java @@ -68,8 +68,6 @@ class MnemonicDelugeEffect extends OneShotEffect { Cards cards = new CardsImpl(); for (int i = 0; i < 3; i++) { Card copiedCard = game.copyCard(card, source, source.getControllerId()); - game.getExile().add(source.getSourceId(), "", copiedCard); - game.getState().setZone(copiedCard.getId(), Zone.EXILED); cards.add(copiedCard); } for (Card copiedCard : cards.getCards(game)) { diff --git a/Mage.Sets/src/mage/cards/n/NullProfusion.java b/Mage.Sets/src/mage/cards/n/NullProfusion.java index 2345bc0f50f..2b4847a26d8 100644 --- a/Mage.Sets/src/mage/cards/n/NullProfusion.java +++ b/Mage.Sets/src/mage/cards/n/NullProfusion.java @@ -2,7 +2,7 @@ package mage.cards.n; import java.util.UUID; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.PlayCardTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.common.DrawCardSourceControllerEffect; @@ -15,9 +15,6 @@ import mage.constants.CardType; import mage.constants.Duration; import mage.constants.TargetController; import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; /** * @@ -32,7 +29,8 @@ public final class NullProfusion extends CardImpl { this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new SkipDrawStepEffect())); // Whenever you play a card, draw a card. - this.addAbility(new NullProfusionTriggeredAbility()); + this.addAbility(new PlayCardTriggeredAbility(TargetController.YOU, Zone.BATTLEFIELD, + new DrawCardSourceControllerEffect(1))); // Your maximum hand size is two. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, @@ -53,35 +51,4 @@ public final class NullProfusion extends CardImpl { public NullProfusion copy() { return new NullProfusion(this); } -} - -class NullProfusionTriggeredAbility extends TriggeredAbilityImpl { - - NullProfusionTriggeredAbility() { - super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false); - } - - NullProfusionTriggeredAbility(final NullProfusionTriggeredAbility ability) { - super(ability); - } - - @Override - public NullProfusionTriggeredAbility copy() { - return new NullProfusionTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.SPELL_CAST || event.getType() == GameEvent.EventType.LAND_PLAYED; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - return event.getPlayerId().equals(this.getControllerId()); - } - - @Override - public String getRule() { - return "Whenever you play a card, draw a card."; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/p/ProsperTomeBound.java b/Mage.Sets/src/mage/cards/p/ProsperTomeBound.java index 9451fade9f7..3e557f4d9e2 100644 --- a/Mage.Sets/src/mage/cards/p/ProsperTomeBound.java +++ b/Mage.Sets/src/mage/cards/p/ProsperTomeBound.java @@ -3,15 +3,18 @@ package mage.cards.p; import mage.MageInt; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.common.PlayCardTriggeredAbility; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.ExileTopXMayPlayUntilEndOfTurnEffect; import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.token.TreasureToken; +import mage.game.stack.Spell; import java.util.UUID; @@ -53,10 +56,10 @@ public final class ProsperTomeBound extends CardImpl { } } -class ProsperTomeBoundTriggeredAbility extends TriggeredAbilityImpl { +class ProsperTomeBoundTriggeredAbility extends PlayCardTriggeredAbility { ProsperTomeBoundTriggeredAbility() { - super(Zone.BATTLEFIELD, new CreateTokenEffect(new TreasureToken())); + super(TargetController.YOU, Zone.BATTLEFIELD, new CreateTokenEffect(new TreasureToken())); this.flavorWord = "Pact Boon"; setTriggerPhrase("Whenever you play a card from exile, "); } @@ -65,15 +68,9 @@ class ProsperTomeBoundTriggeredAbility extends TriggeredAbilityImpl { super(ability); } - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.SPELL_CAST - || event.getType() == GameEvent.EventType.LAND_PLAYED; - } - @Override public boolean checkTrigger(GameEvent event, Game game) { - return isControlledBy(event.getPlayerId()) && event.getZone() == Zone.EXILED; + return super.checkTrigger(event, game) && event.getZone() == Zone.EXILED; } @Override diff --git a/Mage.Sets/src/mage/cards/p/PsionicRitual.java b/Mage.Sets/src/mage/cards/p/PsionicRitual.java index c67679625a7..52cb9c01737 100644 --- a/Mage.Sets/src/mage/cards/p/PsionicRitual.java +++ b/Mage.Sets/src/mage/cards/p/PsionicRitual.java @@ -53,4 +53,4 @@ public final class PsionicRitual extends CardImpl { public PsionicRitual copy() { return new PsionicRitual(this); } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/cards/r/Recycle.java b/Mage.Sets/src/mage/cards/r/Recycle.java index e38c16ff376..fa126a62021 100644 --- a/Mage.Sets/src/mage/cards/r/Recycle.java +++ b/Mage.Sets/src/mage/cards/r/Recycle.java @@ -2,7 +2,7 @@ package mage.cards.r; import java.util.UUID; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.PlayCardTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.SkipDrawStepEffect; @@ -12,10 +12,8 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; +import mage.constants.TargetController; import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; /** * @@ -30,7 +28,8 @@ public final class Recycle extends CardImpl { this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new SkipDrawStepEffect())); // Whenever you play a card, draw a card. - this.addAbility(new RecycleTriggeredAbility()); + this.addAbility(new PlayCardTriggeredAbility(TargetController.YOU, Zone.BATTLEFIELD, + new DrawCardSourceControllerEffect(1))); // Your maximum hand size is two. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new MaximumHandSizeControllerEffect(2, Duration.WhileOnBattlefield, HandSizeModification.SET))); @@ -44,35 +43,4 @@ public final class Recycle extends CardImpl { public Recycle copy() { return new Recycle(this); } -} - -class RecycleTriggeredAbility extends TriggeredAbilityImpl { - - RecycleTriggeredAbility() { - super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false); - } - - RecycleTriggeredAbility(final RecycleTriggeredAbility ability) { - super(ability); - } - - @Override - public RecycleTriggeredAbility copy() { - return new RecycleTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.SPELL_CAST || event.getType() == GameEvent.EventType.LAND_PLAYED; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - return event.getPlayerId().equals(this.getControllerId()); - } - - @Override - public String getRule() { - return "Whenever you play a card, draw a card."; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SearchTheCity.java b/Mage.Sets/src/mage/cards/s/SearchTheCity.java index c3392c28f55..06b52a863d5 100644 --- a/Mage.Sets/src/mage/cards/s/SearchTheCity.java +++ b/Mage.Sets/src/mage/cards/s/SearchTheCity.java @@ -1,14 +1,15 @@ package mage.cards.s; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.PlayCardTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; +import mage.constants.TargetController; import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.predicate.mageobject.NamePredicate; @@ -82,10 +83,10 @@ class SearchTheCityExileEffect extends OneShotEffect { } } -class SearchTheCityTriggeredAbility extends TriggeredAbilityImpl { +class SearchTheCityTriggeredAbility extends PlayCardTriggeredAbility { public SearchTheCityTriggeredAbility() { - super(Zone.BATTLEFIELD, new SearchTheCityExiledCardToHandEffect(), true); + super(TargetController.YOU, Zone.BATTLEFIELD, new SearchTheCityExiledCardToHandEffect(), true); setTriggerPhrase("Whenever you play a card with the same name as one of the exiled cards, " ); } @@ -93,14 +94,9 @@ class SearchTheCityTriggeredAbility extends TriggeredAbilityImpl { super(ability); } - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.SPELL_CAST || event.getType() == GameEvent.EventType.LAND_PLAYED; - } - @Override public boolean checkTrigger(GameEvent event, Game game) { - if (!event.getPlayerId().equals(this.getControllerId())) { + if (!super.checkTrigger(event, game)) { return false; } String cardName = ""; diff --git a/Mage.Sets/src/mage/cards/s/Spellbinder.java b/Mage.Sets/src/mage/cards/s/Spellbinder.java index e875e85ec00..7d4dee86722 100644 --- a/Mage.Sets/src/mage/cards/s/Spellbinder.java +++ b/Mage.Sets/src/mage/cards/s/Spellbinder.java @@ -175,8 +175,6 @@ class SpellbinderCopyEffect extends OneShotEffect { if (controller.chooseUse(outcome, "Create a copy of " + imprintedInstant.getName() + '?', source, game)) { Card copiedCard = game.copyCard(imprintedInstant, source, source.getControllerId()); if (copiedCard != null) { - game.getExile().add(source.getSourceId(), "", copiedCard); - game.getState().setZone(copiedCard.getId(), Zone.EXILED); if (controller.chooseUse(outcome, "Cast the copied card without paying mana cost?", source, game)) { if (copiedCard.getSpellAbility() != null) { game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); diff --git a/Mage.Sets/src/mage/cards/s/SpellweaverVolute.java b/Mage.Sets/src/mage/cards/s/SpellweaverVolute.java index 7fb08bdac15..c08e9e8f3fe 100644 --- a/Mage.Sets/src/mage/cards/s/SpellweaverVolute.java +++ b/Mage.Sets/src/mage/cards/s/SpellweaverVolute.java @@ -90,8 +90,6 @@ class SpellweaverVoluteEffect extends OneShotEffect { && controller.chooseUse(Outcome.Copy, "Create a copy of " + enchantedCard.getName() + '?', source, game)) { Card copiedCard = game.copyCard(enchantedCard, source, source.getControllerId()); if (copiedCard != null) { - controller.getGraveyard().add(copiedCard); - game.getState().setZone(copiedCard.getId(), Zone.GRAVEYARD); if (controller.chooseUse(Outcome.PlayForFree, "Cast the copied card without paying mana cost?", source, game)) { if (copiedCard.getSpellAbility() != null) { game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); diff --git a/Mage.Sets/src/mage/cards/s/SurgeToVictory.java b/Mage.Sets/src/mage/cards/s/SurgeToVictory.java index 3a38ec5c30c..c07e354c205 100644 --- a/Mage.Sets/src/mage/cards/s/SurgeToVictory.java +++ b/Mage.Sets/src/mage/cards/s/SurgeToVictory.java @@ -144,7 +144,6 @@ class SurgeToVictoryCastEffect extends OneShotEffect { if (copiedCard == null) { return false; } - player.moveCards(copiedCard, Zone.EXILED, source, game); if (!player.chooseUse(outcome, "Cast the copy of the exiled card?", source, game)) { return false; } diff --git a/Mage.Sets/src/mage/cards/w/WildfireDevils.java b/Mage.Sets/src/mage/cards/w/WildfireDevils.java index 5c7a4256076..2b7c0002c13 100644 --- a/Mage.Sets/src/mage/cards/w/WildfireDevils.java +++ b/Mage.Sets/src/mage/cards/w/WildfireDevils.java @@ -102,7 +102,6 @@ class WildfireDevilsEffect extends OneShotEffect { if (copiedCard == null) { return false; } - randomPlayer.moveCards(copiedCard, Zone.EXILED, source, game); if (!controller.chooseUse(outcome, "Cast the copy of the exiled card?", source, game)) { return false; } diff --git a/Mage.Sets/src/mage/cards/z/ZethiArcaneBlademaster.java b/Mage.Sets/src/mage/cards/z/ZethiArcaneBlademaster.java index de4b2fbcd49..42b77154525 100644 --- a/Mage.Sets/src/mage/cards/z/ZethiArcaneBlademaster.java +++ b/Mage.Sets/src/mage/cards/z/ZethiArcaneBlademaster.java @@ -131,8 +131,6 @@ class ZethiArcaneBlademasterCastEffect extends OneShotEffect { Cards copies = new CardsImpl(); for (Card card : cards.getCards(game)) { Card copiedCard = game.copyCard(card, source, source.getControllerId()); - game.getExile().add(source.getSourceId(), "", copiedCard); - game.getState().setZone(copiedCard.getId(), Zone.EXILED); copies.add(copiedCard); } // simple way to choose the spells to cast; if you have a better tech, implement it! diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ProsperTomeBoundTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ProsperTomeBoundTest.java new file mode 100644 index 00000000000..8333c901477 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ProsperTomeBoundTest.java @@ -0,0 +1,41 @@ +package org.mage.test.cards.single.afc; + +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import mage.constants.PhaseStep; +import mage.constants.Zone; + +public class ProsperTomeBoundTest extends CardTestPlayerBase { + static final String prosper = "Prosper, Tome-Bound"; + + @Test + // Author: alexander-novo + // As copies of cards aren't themselves cards, and Prosper specifies that his ability only triggers when a *card* is played from exile, + // Prosper shouldn't work with cards a la Mizzix's Mastery, which creates copies of cards in exile and then casts them. + // As of right now, this is true, but I'd guess this is due to the fact that effects like Mizzix's Mastery currently don't cast the copies from exile properly, + // not because Prosper is actually checking that the spells come from actual cards. + public void castCopyFromExileTest() { + String mastery = "Mizzix's Mastery"; + String bolt = "Lightning Bolt"; + + // Cast mastery from hand targetting bolt, which will be exiled, copied, and cast. Prosper will see this cast. + addCard(Zone.GRAVEYARD, playerA, bolt); + addCard(Zone.HAND, playerA, mastery); + addCard(Zone.BATTLEFIELD, playerA, prosper); + + // Enough mana for Mizzix's mastery + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + // Cast mastery. Choose target for bolt + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mastery, bolt); + addTarget(playerA, playerB); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 3); + assertExileCount(playerA, bolt, 1); + assertTokenCount(playerA, "Treasure Token", 0); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c15/MizzixsMasteryTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c15/MizzixsMasteryTest.java new file mode 100644 index 00000000000..35793af7e3a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c15/MizzixsMasteryTest.java @@ -0,0 +1,38 @@ +package org.mage.test.cards.single.c15; + +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import mage.constants.PhaseStep; +import mage.constants.Zone; + +public class MizzixsMasteryTest extends CardTestPlayerBase { + private static final String mastery = "Mizzix's Mastery"; + + @Test + // Author: alexander-novo + // Making sure overload works correctly. + public void overloadTest() { + String fireball = "Delayed Blast Fireball"; + + // Prep for exiling fireball from graveyard, copying, and then casting copy + addCard(Zone.GRAVEYARD, playerA, fireball, 2); + addCard(Zone.HAND, playerA, mastery); + + // Enough mana to overload mastery + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + + // Cast Mizzix's Mastery targetting delayed blast fireball. This should exile it, copy it into exile, and then cast the copy from exile, which should end up dealing 5 damage to player B + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mastery + " with overload"); + addTarget(playerA, fireball, 2); + setChoice(playerA, true); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 2 * 5); + assertExileCount(playerA, fireball, 2); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/clb/DelayedBlastFireballTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/clb/DelayedBlastFireballTest.java new file mode 100644 index 00000000000..617b1f38d55 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/clb/DelayedBlastFireballTest.java @@ -0,0 +1,37 @@ +package org.mage.test.cards.single.clb; + +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import mage.constants.PhaseStep; +import mage.constants.Zone; + +public class DelayedBlastFireballTest extends CardTestPlayerBase { + static final String fireball = "Delayed Blast Fireball"; + + @Test + // Author: alexander-novo + // Issue: magefree/mage#10435 + // If you create a copy of a card in exile and then cast that copy, it should be cast from exile. + // But if we do this with Delayed Blast Fireball (and Mizzix's Mastery, for instance), it won't deal the extra damage for casting from exile. + public void testCopyCardAndCastFromExile() { + String mastery = "Mizzix's Mastery"; + + // Prep for exiling fireball from graveyard, copying, and then casting copy + addCard(Zone.GRAVEYARD, playerA, fireball); + addCard(Zone.HAND, playerA, mastery); + + // Enough mana to cast mastery + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + // Cast Mizzix's Mastery targetting delayed blast fireball. This hsoul exile it, copy it into exile, and then cast the copy from exile, which should end up dealing 5 damage to player B + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mastery, fireball); + setChoice(playerA, true); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 5); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/PlayCardTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/PlayCardTriggeredAbility.java new file mode 100644 index 00000000000..f79ca8ca822 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/PlayCardTriggeredAbility.java @@ -0,0 +1,105 @@ +package mage.abilities.common; + +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; + +// Author: alexander-novo +// A triggered ability for cards which say "whenever play(s) a card..." +public class PlayCardTriggeredAbility extends TriggeredAbilityImpl { + + private final TargetController targetController; + + /** + * + * @param targetController Which player(s) playing cards can trigger this ability. Only [ANY, NOT_YOU, OPPONENT, YOU] are supported. + * @param zone + * @param effect + */ + public PlayCardTriggeredAbility(TargetController targetController, Zone zone, Effect effect) { + super(zone, effect); + this.targetController = targetController; + + constructTriggerPhrase(); + } + + /** + * + * @param targetController Which player(s) playing cards can trigger this ability. Only [ANY, NOT_YOU, OPPONENT, YOU] are supported. + * @param zone + * @param effect + * @param optional + */ + public PlayCardTriggeredAbility(TargetController targetController, Zone zone, Effect effect, boolean optional) { + super(zone, effect, optional); + this.targetController = targetController; + + constructTriggerPhrase(); + } + + private void constructTriggerPhrase() { + switch (targetController) { + case ANY: + setTriggerPhrase("Whenever a player plays play a card, "); + break; + case NOT_YOU: + setTriggerPhrase("Whenever another player plays a card, "); + break; + case OPPONENT: + setTriggerPhrase("Whenever an opponent plays a card, "); + break; + case YOU: + setTriggerPhrase("Whenever you play a card, "); + break; + default: + throw new UnsupportedOperationException("TargetController not supported"); + } + } + + public PlayCardTriggeredAbility(final PlayCardTriggeredAbility ability) { + super(ability); + + this.targetController = ability.targetController; + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.SPELL_CAST + || event.getType() == GameEvent.EventType.LAND_PLAYED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + boolean playerMatches; + switch (targetController) { + case ANY: + playerMatches = true; + break; + case NOT_YOU: + playerMatches = !isControlledBy(event.getPlayerId()); + break; + case OPPONENT: + playerMatches = game.getPlayer(getControllerId()).hasOpponent(event.getPlayerId(), game); + break; + case YOU: + playerMatches = isControlledBy(event.getPlayerId()); + break; + default: + throw new UnsupportedOperationException("TargetController not supported"); + } + + // Make sure that, if a spell was cast, it came from an actual card (and not a copy of a card) + return playerMatches && (event.getType() != GameEvent.EventType.SPELL_CAST + || !game.getSpell(event.getTargetId()).getCard().isCopy()); + } + + @Override + public TriggeredAbility copy() { + return new PlayCardTriggeredAbility(this); + } + +} diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index 790d51e8ce2..b2948559692 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -445,89 +445,94 @@ public abstract class CardImpl extends MageObjectImpl implements Card { public boolean removeFromZone(Game game, Zone fromZone, Ability source) { boolean removed = false; MageObject lkiObject = null; - switch (fromZone) { - case GRAVEYARD: - removed = game.getPlayer(ownerId).removeFromGraveyard(this, game); - break; - case HAND: - removed = game.getPlayer(ownerId).removeFromHand(this, game); - break; - case LIBRARY: - removed = game.getPlayer(ownerId).removeFromLibrary(this, game); - break; - case EXILED: - if (game.getExile().getCard(getId(), game) != null) { - removed = game.getExile().removeCard(this, game); - } - break; - case STACK: - StackObject stackObject; - if (getSpellAbility() != null) { - stackObject = game.getStack().getSpell(getSpellAbility().getId(), false); - } else { - stackObject = game.getStack().getSpell(this.getId(), false); - } + if (isCopy()) { // copied cards have no need to be removed from a previous zone + removed = true; + } else { + switch (fromZone) { + case GRAVEYARD: + removed = game.getPlayer(ownerId).removeFromGraveyard(this, game); + break; + case HAND: + removed = game.getPlayer(ownerId).removeFromHand(this, game); + break; + case LIBRARY: + removed = game.getPlayer(ownerId).removeFromLibrary(this, game); + break; + case EXILED: + if (game.getExile().getCard(getId(), game) != null) { + removed = game.getExile().removeCard(this, game); + } + break; + case STACK: + StackObject stackObject; + if (getSpellAbility() != null) { + stackObject = game.getStack().getSpell(getSpellAbility().getId(), false); + } else { + stackObject = game.getStack().getSpell(this.getId(), false); + } + + // handle half of Split Cards on stack + if (stackObject == null && (this instanceof SplitCard)) { + stackObject = game.getStack().getSpell(((SplitCard) this).getLeftHalfCard().getId(), false); + if (stackObject == null) { + stackObject = game.getStack().getSpell(((SplitCard) this).getRightHalfCard().getId(), + false); + } + } + + // handle half of Modal Double Faces Cards on stack + if (stackObject == null && (this instanceof ModalDoubleFacedCard)) { + stackObject = game.getStack().getSpell(((ModalDoubleFacedCard) this).getLeftHalfCard().getId(), + false); + if (stackObject == null) { + stackObject = game.getStack() + .getSpell(((ModalDoubleFacedCard) this).getRightHalfCard().getId(), false); + } + } + + if (stackObject == null && (this instanceof AdventureCard)) { + stackObject = game.getStack().getSpell(((AdventureCard) this).getSpellCard().getId(), false); + } - // handle half of Split Cards on stack - if (stackObject == null && (this instanceof SplitCard)) { - stackObject = game.getStack().getSpell(((SplitCard) this).getLeftHalfCard().getId(), false); if (stackObject == null) { - stackObject = game.getStack().getSpell(((SplitCard) this).getRightHalfCard().getId(), false); + stackObject = game.getStack().getSpell(getId(), false); } - } - - // handle half of Modal Double Faces Cards on stack - if (stackObject == null && (this instanceof ModalDoubleFacedCard)) { - stackObject = game.getStack().getSpell(((ModalDoubleFacedCard) this).getLeftHalfCard().getId(), false); - if (stackObject == null) { - stackObject = game.getStack().getSpell(((ModalDoubleFacedCard) this).getRightHalfCard().getId(), false); + if (stackObject != null) { + removed = game.getStack().remove(stackObject, game); + lkiObject = stackObject; } - } - - if (stackObject == null && (this instanceof AdventureCard)) { - stackObject = game.getStack().getSpell(((AdventureCard) this).getSpellCard().getId(), false); - } - - if (stackObject == null) { - stackObject = game.getStack().getSpell(getId(), false); - } - if (stackObject != null) { - removed = game.getStack().remove(stackObject, game); - lkiObject = stackObject; - } - break; - case COMMAND: - for (CommandObject commandObject : game.getState().getCommand()) { - if (commandObject.getId().equals(objectId)) { - lkiObject = commandObject; + break; + case COMMAND: + for (CommandObject commandObject : game.getState().getCommand()) { + if (commandObject.getId().equals(objectId)) { + lkiObject = commandObject; + } } - } - if (lkiObject != null) { - removed = game.getState().getCommand().remove(lkiObject); - } - break; - case OUTSIDE: - if (isCopy()) { // copied cards have no need to be removed from a previous zone + if (lkiObject != null) { + removed = game.getState().getCommand().remove(lkiObject); + } + break; + case OUTSIDE: + if (game.getPlayer(ownerId).getSideboard().contains(this.getId())) { + game.getPlayer(ownerId).getSideboard().remove(this.getId()); + removed = true; + } else if (game.getPhase() == null) { + // E.g. Commander of commander game + removed = true; + } else { + // Unstable - Summon the Pack + removed = true; + } + break; + case BATTLEFIELD: // for sacrificing permanents or putting to library removed = true; - } else if (game.getPlayer(ownerId).getSideboard().contains(this.getId())) { - game.getPlayer(ownerId).getSideboard().remove(this.getId()); - removed = true; - } else if (game.getPhase() == null) { - // E.g. Commander of commander game - removed = true; - } else { - // Unstable - Summon the Pack - removed = true; - } - break; - case BATTLEFIELD: // for sacrificing permanents or putting to library - removed = true; - break; - default: - MageObject sourceObject = game.getObject(source); - logger.fatal("Invalid from zone [" + fromZone + "] for card [" + this.getIdName() - + "] source [" + (sourceObject != null ? sourceObject.getName() : "null") + ']'); - break; + break; + default: + MageObject sourceObject = game.getObject(source); + logger.fatal("Invalid from zone [" + fromZone + "] for card [" + this.getIdName() + + "] source [" + (sourceObject != null ? sourceObject.getName() : "null") + ']'); + break; + } } if (removed) { if (fromZone != Zone.OUTSIDE) { diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 3115feabc06..0352fa6aaa9 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -1388,10 +1388,16 @@ public class GameState implements Serializable, Copyable { // main part prepare (must be called after other parts cause it change ids for all) prepareCardForCopy(mainCardToCopy, copiedCard, newController); + // 707.12. An effect that instructs a player to cast a copy of an object (and not just copy a spell) follows the rules for casting spells, except that the copy is created in the same zone the object is in and then cast while another spell or ability is resolving. + Zone copyToZone = game.getState().getZone(mainCardToCopy.getId()); + if (copyToZone == Zone.BATTLEFIELD) { + throw new UnsupportedOperationException("Cards cannot be copied while on the Battlefield"); + } + // add all parts to the game copiedParts.forEach(card -> { copiedCards.put(card.getId(), card); - addCard(card); + addCard(card, copyToZone); }); // copied cards removes from game after battlefield/stack leaves, so remember it here as workaround to fix freeze, see https://github.com/magefree/mage/issues/5437