diff --git a/Mage.Sets/src/mage/cards/a/ArcaneProxy.java b/Mage.Sets/src/mage/cards/a/ArcaneProxy.java index be0c9ee38a0..bd89b0c870a 100644 --- a/Mage.Sets/src/mage/cards/a/ArcaneProxy.java +++ b/Mage.Sets/src/mage/cards/a/ArcaneProxy.java @@ -1,26 +1,23 @@ package mage.cards.a; -import mage.ApprovingObject; import mage.MageInt; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.CastFromEverywhereSourceCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.keyword.PrototypeAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.SubType; import mage.filter.FilterCard; import mage.filter.common.FilterInstantOrSorceryCard; import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; -import mage.players.Player; import mage.target.common.TargetCardInYourGraveyard; import java.util.Objects; @@ -52,7 +49,7 @@ public final class ArcaneProxy extends CardImpl { // When Arcane Proxy enters the battlefield, if you cast it, exile target instant or sorcery card with mana value less than or equal to Arcane Proxy's power from your graveyard. Copy that card. You may cast the copy without paying its mana cost. Ability ability = new ConditionalInterveningIfTriggeredAbility( - new EntersBattlefieldTriggeredAbility(new ArcaneProxyEffect()), + new EntersBattlefieldTriggeredAbility(new ExileTargetCardCopyAndCastEffect(false)), CastFromEverywhereSourceCondition.instance, "When {this} enters the battlefield, " + "if you cast it, exile target instant or sorcery card with mana value less than or equal to {this}'s " + "power from your graveyard. Copy that card. You may cast the copy without paying its mana cost." @@ -84,39 +81,4 @@ enum ArcaneProxyPredicate implements ObjectSourcePlayerPredicate { .map(p -> input.getObject().getManaValue() <= p) .orElse(false); } -} - -class ArcaneProxyEffect extends OneShotEffect { - - ArcaneProxyEffect() { - super(Outcome.Benefit); - } - - private ArcaneProxyEffect(final ArcaneProxyEffect effect) { - super(effect); - } - - @Override - public ArcaneProxyEffect copy() { - return new ArcaneProxyEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - Card card = game.getCard(getTargetPointer().getFirst(game, source)); - if (player == null || card == null || !player.chooseUse( - outcome, "Cast a copy of " + card.getName() + '?', source, game - )) { - return false; - } - Card copiedCard = game.copyCard(card, source, player.getId()); - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); - player.cast( - player.chooseAbilityForCast(copiedCard, game, false), - game, false, new ApprovingObject(source, game) - ); - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), null); - return true; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/d/Demilich.java b/Mage.Sets/src/mage/cards/d/Demilich.java index 1a0aa25a236..f242a6cc115 100644 --- a/Mage.Sets/src/mage/cards/d/Demilich.java +++ b/Mage.Sets/src/mage/cards/d/Demilich.java @@ -2,7 +2,6 @@ package mage.cards.d; import java.util.UUID; -import mage.ApprovingObject; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; @@ -15,10 +14,9 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.Effect; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; import mage.abilities.hint.ValueHint; -import mage.cards.Card; import mage.constants.*; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -49,7 +47,8 @@ public final class Demilich extends CardImpl { )).addHint(new ValueHint("Instants and sorceries you've cast this turn", DemilichValue.instance)), new SpellsCastWatcher()); // Whenever Demilich attacks, exile up to one target instant or sorcery card from your graveyard. Copy it. You may cast the copy. - Ability ability = new AttacksTriggeredAbility(new DemilichCopyEffect()); + Ability ability = new AttacksTriggeredAbility(new ExileTargetCardCopyAndCastEffect(false).setText( + "exile up to one target instant or sorcery card from your graveyard. Copy it. You may cast the copy")); ability.addTarget(new TargetCardInYourGraveyard(0, 1, StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD)); this.addAbility(ability); @@ -95,41 +94,6 @@ enum DemilichValue implements DynamicValue { } } -class DemilichCopyEffect extends OneShotEffect { - - public DemilichCopyEffect() { - super(Outcome.Benefit); - this.staticText = "exile up to one target instant or sorcery card from your graveyard. Copy it. You may cast the copy"; - } - - private DemilichCopyEffect(final DemilichCopyEffect effect) { - super(effect); - } - - @Override - public DemilichCopyEffect copy() { - return new DemilichCopyEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - Card card = game.getCard(targetPointer.getFirst(game, source)); - if (controller == null || card == null) { - return false; - } - controller.moveCards(card, Zone.EXILED, source, game); - if (controller.chooseUse(outcome, "Cast copy of " + card.getName() + '?', source, game)) { - Card copiedCard = game.copyCard(card, source, controller.getId()); - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); - controller.cast(controller.chooseAbilityForCast(copiedCard, game, false), - game, false, new ApprovingObject(source, game)); - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), null); - } - return true; - } -} - class DemilichPlayEffect extends AsThoughEffectImpl { public DemilichPlayEffect() { diff --git a/Mage.Sets/src/mage/cards/f/FlawlessForgery.java b/Mage.Sets/src/mage/cards/f/FlawlessForgery.java index 71305418137..fde55885cda 100644 --- a/Mage.Sets/src/mage/cards/f/FlawlessForgery.java +++ b/Mage.Sets/src/mage/cards/f/FlawlessForgery.java @@ -1,20 +1,13 @@ package mage.cards.f; -import mage.ApprovingObject; -import mage.abilities.Ability; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.keyword.CasualtyAbility; -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.common.FilterInstantOrSorceryCard; -import mage.game.Game; -import mage.players.Player; import mage.target.common.TargetCardInGraveyard; import java.util.UUID; @@ -38,7 +31,8 @@ public final class FlawlessForgery extends CardImpl { this.addAbility(new CasualtyAbility(3)); // Exile target instant or sorcery card from an opponent's graveyard. Copy that card. You may cast the copy without paying its mana cost. - this.getSpellAbility().addEffect(new FlawlessForgeryEffect()); + this.getSpellAbility().addEffect(new ExileTargetCardCopyAndCastEffect(true).setText( + "Exile target instant or sorcery card from an opponent's graveyard. Copy that card. You may cast the copy without paying its mana cost.")); this.getSpellAbility().addTarget(new TargetCardInGraveyard(filter)); } @@ -50,44 +44,4 @@ public final class FlawlessForgery extends CardImpl { public FlawlessForgery copy() { return new FlawlessForgery(this); } -} - -class FlawlessForgeryEffect extends OneShotEffect { - - FlawlessForgeryEffect() { - super(Outcome.Benefit); - staticText = "exile target instant or sorcery card from an opponent's graveyard. " + - "Copy that card. You may cast the copy without paying its mana cost"; - } - - private FlawlessForgeryEffect(final FlawlessForgeryEffect effect) { - super(effect); - } - - @Override - public FlawlessForgeryEffect copy() { - return new FlawlessForgeryEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - Card card = game.getCard(getTargetPointer().getFirst(game, source)); - if (player == null || card == null) { - return false; - } - player.moveCards(card, Zone.EXILED, source, game); - Card cardCopy = game.copyCard(card, source, source.getControllerId()); - if (!player.chooseUse(outcome, "Cast copy of " + - card.getName() + " without paying its mana cost?", source, game)) { - return true; - } - game.getState().setValue("PlayFromNotOwnHandZone" + cardCopy.getId(), Boolean.TRUE); - player.cast( - player.chooseAbilityForCast(cardCopy, game, true), - game, true, new ApprovingObject(source, game) - ); - game.getState().setValue("PlayFromNotOwnHandZone" + cardCopy.getId(), null); - return true; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/f/FoundingTheThirdPath.java b/Mage.Sets/src/mage/cards/f/FoundingTheThirdPath.java index 3c93402cb48..29380254d1f 100644 --- a/Mage.Sets/src/mage/cards/f/FoundingTheThirdPath.java +++ b/Mage.Sets/src/mage/cards/f/FoundingTheThirdPath.java @@ -1,12 +1,9 @@ package mage.cards.f; -import mage.ApprovingObject; -import mage.abilities.Ability; import mage.abilities.common.SagaAbility; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.effects.common.MillCardsTargetEffect; import mage.abilities.effects.common.cost.CastFromHandForFreeEffect; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -14,8 +11,6 @@ import mage.filter.FilterCard; import mage.filter.StaticFilters; import mage.filter.common.FilterInstantOrSorceryCard; import mage.filter.predicate.mageobject.ManaValuePredicate; -import mage.game.Game; -import mage.players.Player; import mage.target.TargetPlayer; import mage.target.common.TargetCardInYourGraveyard; @@ -54,7 +49,8 @@ public final class FoundingTheThirdPath extends CardImpl { // III -- Exile target instant or sorcery card from your graveyard. Copy it. You may cast the copy. sagaAbility.addChapterEffect( this, SagaChapter.CHAPTER_III, SagaChapter.CHAPTER_III, - new FoundingTheThirdPathEffect(), + new ExileTargetCardCopyAndCastEffect(false).setText( + "exile target instant or sorcery card from your graveyard. Copy it. You may cast the copy"), new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD) ); this.addAbility(sagaAbility); @@ -68,42 +64,4 @@ public final class FoundingTheThirdPath extends CardImpl { public FoundingTheThirdPath copy() { return new FoundingTheThirdPath(this); } -} - -class FoundingTheThirdPathEffect extends OneShotEffect { - - FoundingTheThirdPathEffect() { - super(Outcome.Benefit); - staticText = "exile target instant or sorcery card from your graveyard. Copy it. You may cast the copy"; - } - - private FoundingTheThirdPathEffect(final FoundingTheThirdPathEffect effect) { - super(effect); - } - - @Override - public FoundingTheThirdPathEffect copy() { - return new FoundingTheThirdPathEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - Card card = game.getCard(getTargetPointer().getFirst(game, source)); - if (controller == null || card == null) { - return false; - } - controller.moveCards(card, Zone.EXILED, source, game); - if (!controller.chooseUse(outcome, "Cast copy of " + card.getName() + '?', source, game)) { - return true; - } - Card copiedCard = game.copyCard(card, source, controller.getId()); - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); - controller.cast( - controller.chooseAbilityForCast(copiedCard, game, false), - game, false, new ApprovingObject(source, game) - ); - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), null); - return true; - } -} +} \ 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 89ec5c8073c..ba8b0075d8d 100644 --- a/Mage.Sets/src/mage/cards/m/MizzixsMastery.java +++ b/Mage.Sets/src/mage/cards/m/MizzixsMastery.java @@ -4,6 +4,7 @@ import mage.abilities.Ability; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ExileSpellEffect; +import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.keyword.OverloadAbility; import mage.cards.*; import mage.constants.CardType; @@ -31,7 +32,8 @@ public final class MizzixsMastery extends CardImpl { // Exile target card that's an instant or sorcery from your graveyard. // For each card exiled this way, copy it, and you may cast the copy // without paying its mana cost. Exile Mizzix's Mastery. - this.getSpellAbility().addEffect(new MizzixsMasteryEffect()); + this.getSpellAbility().addEffect(new ExileTargetCardCopyAndCastEffect(true).setText( + "Exile target card that's an instant or sorcery from your graveyard. For each card exiled this way, copy it, and you may cast the copy without paying its mana cost. Exile Mizzix's Mastery.")); this.getSpellAbility().addTarget(new TargetCardInYourGraveyard( new FilterInstantOrSorceryCard("card that's an instant or sorcery from your graveyard"))); this.getSpellAbility().addEffect(new ExileSpellEffect()); @@ -53,48 +55,6 @@ public final class MizzixsMastery extends CardImpl { } } -class MizzixsMasteryEffect extends OneShotEffect { - - public MizzixsMasteryEffect() { - super(Outcome.PlayForFree); - this.staticText = "Exile target card that's an instant or sorcery from your " - + "graveyard. For each card exiled this way, copy it, and you " - + "may cast the copy without paying its mana cost"; - } - - public MizzixsMasteryEffect(final MizzixsMasteryEffect effect) { - super(effect); - } - - @Override - public MizzixsMasteryEffect copy() { - return new MizzixsMasteryEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - Card card = game.getCard(getTargetPointer().getFirst(game, source)); - if (card != null) { - if (controller.moveCards(card, Zone.EXILED, source, game)) { - Card cardCopy = game.copyCard(card, source, source.getControllerId()); - if (cardCopy.getSpellAbility().canChooseTarget(game, controller.getId()) - && controller.chooseUse(outcome, "Cast copy of " - + card.getName() + " without paying its mana cost?", source, game)) { - game.getState().setValue("PlayFromNotOwnHandZone" + cardCopy.getId(), Boolean.TRUE); - controller.cast(controller.chooseAbilityForCast(cardCopy, game, true), - game, true, new ApprovingObject(source, game)); - game.getState().setValue("PlayFromNotOwnHandZone" + cardCopy.getId(), null); - } - } - } - return true; - } - return false; - } -} - class MizzixsMasteryOverloadEffect extends OneShotEffect { public MizzixsMasteryOverloadEffect() { diff --git a/Mage.Sets/src/mage/cards/n/NarsetEnlightenedExile.java b/Mage.Sets/src/mage/cards/n/NarsetEnlightenedExile.java index 6ed7cc89010..c9f1c13b644 100644 --- a/Mage.Sets/src/mage/cards/n/NarsetEnlightenedExile.java +++ b/Mage.Sets/src/mage/cards/n/NarsetEnlightenedExile.java @@ -1,12 +1,11 @@ package mage.cards.n; -import mage.ApprovingObject; import mage.MageInt; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; import mage.abilities.keyword.ProwessAbility; import mage.cards.Card; @@ -20,7 +19,6 @@ import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.Predicates; import mage.game.Game; -import mage.players.Player; import mage.target.common.TargetCardInGraveyard; import java.util.Objects; @@ -56,7 +54,9 @@ public final class NarsetEnlightenedExile extends CardImpl { ).setText("creatures you control have prowess"))); // Whenever Narset, Enlightened Exile attacks, exile target noncreature, nonland card with mana value less than Narset's power from a graveyard and copy it. You may cast the copy without paying its mana cost. - Ability ability = new AttacksTriggeredAbility(new NarsetEnlightenedExileEffect()); + Ability ability = new AttacksTriggeredAbility(new ExileTargetCardCopyAndCastEffect(true) + .setText("exile target noncreature, nonland card with mana value less than {this}'s power " + + "from a graveyard and copy it. You may cast the copy without paying its mana cost")); ability.addTarget(new TargetCardInGraveyard(filter)); this.addAbility(ability); } @@ -84,40 +84,4 @@ enum NarsetEnlightenedExilePredicate implements ObjectSourcePlayerPredicate input.getObject().getManaValue() < p) .orElse(false); } -} - -class NarsetEnlightenedExileEffect extends OneShotEffect { - - NarsetEnlightenedExileEffect() { - super(Outcome.Benefit); - staticText = "exile target noncreature, nonland card with mana value less than {this}'s power " + - "from a graveyard and copy it. You may cast the copy without paying its mana cost"; - } - - private NarsetEnlightenedExileEffect(final NarsetEnlightenedExileEffect effect) { - super(effect); - } - - @Override - public NarsetEnlightenedExileEffect copy() { - return new NarsetEnlightenedExileEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - Card card = game.getCard(getTargetPointer().getFirst(game, source)); - if (player == null || card == null) { - return false; - } - player.moveCards(card, Zone.EXILED, source, game); - Card copiedCard = game.copyCard(card, source, source.getControllerId()); - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); - player.cast( - player.chooseAbilityForCast(copiedCard, game, true), - game, true, new ApprovingObject(source, game) - ); - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), null); - return true; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/n/NashiMoonsLegacy.java b/Mage.Sets/src/mage/cards/n/NashiMoonsLegacy.java index 10d38ef09f1..8e3bbca3b1a 100644 --- a/Mage.Sets/src/mage/cards/n/NashiMoonsLegacy.java +++ b/Mage.Sets/src/mage/cards/n/NashiMoonsLegacy.java @@ -1,21 +1,17 @@ package mage.cards.n; -import mage.ApprovingObject; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.keyword.MenaceAbility; import mage.abilities.keyword.WardAbility; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.FilterCard; import mage.filter.predicate.Predicates; -import mage.game.Game; -import mage.players.Player; import mage.target.common.TargetCardInYourGraveyard; import java.util.UUID; @@ -50,7 +46,8 @@ public final class NashiMoonsLegacy extends CardImpl { this.addAbility(new WardAbility(new ManaCostsImpl<>("{1}"), false)); // Whenever Nashi, Moon's Legacy attacks, exile up to one target legendary or Rat card from your graveyard and copy it. You may cast the copy. - Ability ability = new AttacksTriggeredAbility(new NashiMoonsLegacyEffect()); + Ability ability = new AttacksTriggeredAbility(new ExileTargetCardCopyAndCastEffect(false).setText( + "exile up to one target legendary or Rat card from your graveyard and copy it. You may cast the copy")); ability.addTarget(new TargetCardInYourGraveyard(0, 1, filter)); this.addAbility(ability); } @@ -63,39 +60,4 @@ public final class NashiMoonsLegacy extends CardImpl { public NashiMoonsLegacy copy() { return new NashiMoonsLegacy(this); } -} - -class NashiMoonsLegacyEffect extends OneShotEffect { - - NashiMoonsLegacyEffect() { - super(Outcome.Benefit); - staticText = "exile up to one target legendary or Rat card from your graveyard and copy it. You may cast the copy"; - } - - private NashiMoonsLegacyEffect(final NashiMoonsLegacyEffect effect) { - super(effect); - } - - @Override - public NashiMoonsLegacyEffect copy() { - return new NashiMoonsLegacyEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - Card card = game.getCard(getTargetPointer().getFirst(game, source)); - if (player == null || card == null) { - return false; - } - player.moveCards(card, Zone.EXILED, source, game); - Card copiedCard = game.copyCard(card, source, player.getId()); - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); - player.cast( - player.chooseAbilityForCast(copiedCard, game, false), - game, false, new ApprovingObject(source, game) - ); - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), null); - return true; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/p/PsionicRitual.java b/Mage.Sets/src/mage/cards/p/PsionicRitual.java index f4d2fb3bdcf..c67679625a7 100644 --- a/Mage.Sets/src/mage/cards/p/PsionicRitual.java +++ b/Mage.Sets/src/mage/cards/p/PsionicRitual.java @@ -1,23 +1,16 @@ package mage.cards.p; -import mage.ApprovingObject; -import mage.abilities.Ability; import mage.abilities.costs.common.TapTargetCost; -import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ExileSpellEffect; +import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.keyword.ReplicateAbility; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.SubType; -import mage.constants.Zone; import mage.filter.StaticFilters; import mage.filter.common.FilterControlledPermanent; import mage.filter.predicate.permanent.TappedPredicate; -import mage.game.Game; -import mage.players.Player; import mage.target.common.TargetCardInGraveyard; import mage.target.common.TargetControlledPermanent; @@ -42,7 +35,10 @@ public final class PsionicRitual extends CardImpl { this.addAbility(new ReplicateAbility(new TapTargetCost(new TargetControlledPermanent(filter)))); // Exile target instant or sorcery card from a graveyard and copy it. You may cast the copy without paying its mana cost. - this.getSpellAbility().addEffect(new PsionicRitualEffect()); + this.getSpellAbility() + .addEffect(new ExileTargetCardCopyAndCastEffect(true) + .setText("exile target instant or sorcery card from a graveyard " + + "and copy it. You may cast the copy without paying its mana cost")); this.getSpellAbility().addTarget(new TargetCardInGraveyard(StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY)); // Exile Psionic Ritual. @@ -57,50 +53,4 @@ public final class PsionicRitual extends CardImpl { public PsionicRitual copy() { return new PsionicRitual(this); } -} - -class PsionicRitualEffect extends OneShotEffect { - - PsionicRitualEffect() { - super(Outcome.Benefit); - staticText = "exile target instant or sorcery card from a graveyard " + - "and copy it. You may cast the copy without paying its mana cost"; - } - - private PsionicRitualEffect(final PsionicRitualEffect effect) { - super(effect); - } - - @Override - public PsionicRitualEffect copy() { - return new PsionicRitualEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - Card card = game.getCard(getTargetPointer().getFirst(game, source)); - if (player == null || card == null) { - return false; - } - player.moveCards(card, Zone.EXILED, source, game); - Card copiedCard = game.copyCard(card, source, source.getControllerId()); - if (copiedCard == null) { - return false; - } - game.getExile().add(source.getSourceId(), "", copiedCard); - game.getState().setZone(copiedCard.getId(), Zone.EXILED); - if (copiedCard.getSpellAbility() == null || !player.chooseUse( - outcome, "Cast the copied card without paying mana cost?", source, game - )) { - return false; - } - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE); - player.cast( - player.chooseAbilityForCast(copiedCard, game, true), - game, true, new ApprovingObject(source, game) - ); - game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), null); - return true; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SarumanOfManyColors.java b/Mage.Sets/src/mage/cards/s/SarumanOfManyColors.java new file mode 100644 index 00000000000..b5a0554bd20 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SarumanOfManyColors.java @@ -0,0 +1,125 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.CastSecondSpellTriggeredAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; +import mage.abilities.keyword.WardAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.common.FilterPermanentCard; +import mage.filter.predicate.Predicate; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.players.Player; +import mage.target.common.TargetCardInHand; +import mage.target.common.TargetCardInOpponentsGraveyard; + +import java.util.UUID; + +/** + * @author alexander-novo + */ +public final class SarumanOfManyColors extends CardImpl { + + static final FilterCard filter = new FilterCard("enchantment, instant, or sorcery card"); + public static final Predicate predicate = Predicates.or( + CardType.ENCHANTMENT.getPredicate(), + CardType.INSTANT.getPredicate(), + CardType.SORCERY.getPredicate()); + + static { + filter.add(predicate); + } + + public SarumanOfManyColors(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[] { CardType.CREATURE }, "{3}{W}{U}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.AVATAR); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(5); + this.toughness = new MageInt(4); + + // Ward—Discard an enchantment, instant, or sorcery card. + this.addAbility(new WardAbility(new DiscardTargetCost(new TargetCardInHand(filter)), false)); + + // Whenever you cast your second spell each turn, each opponent mills two cards. When one or more cards are milled this way, exile target enchantment, instant, or sorcery card with equal or lesser mana value than that spell from an opponent's graveyard. Copy the exiled card. You may cast the copy without paying its mana cost. + this.addAbility(new CastSecondSpellTriggeredAbility(Zone.BATTLEFIELD, new SarumanOfManyColorsEffect(), + TargetController.YOU, false, SetTargetPointer.SPELL)); + } + + private SarumanOfManyColors(final SarumanOfManyColors card) { + super(card); + } + + @Override + public SarumanOfManyColors copy() { + return new SarumanOfManyColors(this); + } +} + +class SarumanOfManyColorsEffect extends OneShotEffect { + + public SarumanOfManyColorsEffect() { + super(Outcome.PlayForFree); + this.staticText = "each opponent mills two cards. When one or more cards are milled this way, exile target enchantment, instant, or sorcery card with equal or lesser mana value than that spell from an opponent's graveyard. Copy the exiled card. You may cast the copy without paying its mana cost."; + } + + public SarumanOfManyColorsEffect(final SarumanOfManyColorsEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + // Keep track of whether or not the reflexive trigger needs to happen + boolean trigger_second_part = false; + + // Each opponent mills two cards + for (UUID playerId : game.getOpponents(source.getControllerId())) { + Player player = game.getPlayer(playerId); + if (player != null) { + // Only do the reflexive trigger if one or more cards were actually milled + trigger_second_part |= !player.millCards(2, source, game).isEmpty(); + } + } + + if (!trigger_second_part) { + return true; + } + + // The second spell cast is just referenced, not targetted, so we might need to get LKI if the spell doesn't exist anymore (such as if it were countered) + Spell spell = game.getSpellOrLKIStack(this.getTargetPointer().getFirst(game, source)); + + // Create a filter for "enchantment, instant, or sorcery card with equal or lesser mana value than that spell" + FilterCard filter = new FilterCard( + "enchantment, instant, or sorcery card with equal or lesser mana value than that spell from an opponent's graveyard"); + filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, spell.getManaValue() + 1)); + filter.add(SarumanOfManyColors.predicate); + + ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility( + new ExileTargetCardCopyAndCastEffect(true), false, + "When one or more cards are milled this way, exile target enchantment, instant, or sorcery card with equal or lesser mana value than that spell from an opponent's graveyard." + + "Copy the exiled card. You may cast the copy without paying its mana cost."); + ability.addTarget(new TargetCardInOpponentsGraveyard(filter)); + + game.fireReflexiveTriggeredAbility(ability, source); + + return true; + } + + @Override + public SarumanOfManyColorsEffect copy() { + return new SarumanOfManyColorsEffect(this); + } + +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java b/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java index 9b359211ada..f2426453ea4 100644 --- a/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java +++ b/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java @@ -109,6 +109,7 @@ public final class TheLordOfTheRingsTalesOfMiddleEarth extends ExpansionSet { cards.add(new SetCardInfo("Rosie Cotton of South Lane", 27, Rarity.UNCOMMON, mage.cards.r.RosieCottonOfSouthLane.class)); cards.add(new SetCardInfo("Samwise Gamgee", 222, Rarity.RARE, mage.cards.s.SamwiseGamgee.class)); cards.add(new SetCardInfo("Samwise the Stouthearted", 28, Rarity.UNCOMMON, mage.cards.s.SamwiseTheStouthearted.class)); + cards.add(new SetCardInfo("Saruman of Many Colors", 223, Rarity.UNCOMMON, mage.cards.s.SarumanOfManyColors.class)); cards.add(new SetCardInfo("Saruman the White", 67, Rarity.UNCOMMON, mage.cards.s.SarumanTheWhite.class)); cards.add(new SetCardInfo("Saruman's Trickery", 68, Rarity.UNCOMMON, mage.cards.s.SarumansTrickery.class)); cards.add(new SetCardInfo("Sauron, the Lidless Eye", 288, Rarity.MYTHIC, mage.cards.s.SauronTheLidlessEye.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/SarumanOfManyColorsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/SarumanOfManyColorsTest.java new file mode 100644 index 00000000000..6689f319485 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/SarumanOfManyColorsTest.java @@ -0,0 +1,226 @@ +package org.mage.test.cards.single.ltr; + +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import mage.constants.PhaseStep; +import mage.constants.Zone; + +public class SarumanOfManyColorsTest extends CardTestPlayerBase { + + static final String saruman = "Saruman of Many Colors"; + + @Test + // Author: alexander-novo + // A simple test to make sure the ward ability is working correctly + public void wardTest() { + String bolt = "Lightning Bolt"; + addCard(Zone.BATTLEFIELD, playerB, saruman); + // Two bolts for casting on Saruman, one for discarding + addCard(Zone.HAND, playerA, bolt, 3); + // A red herring card to ignore on the second cast + addCard(Zone.HAND, playerA, "Mountain"); + + // Mana for casting 2 lightning bolts + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, saruman, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, saruman, true); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertHandCount(playerA, bolt, 0); + assertGraveyardCount(playerA, bolt, 3); + assertDamageReceived(playerB, saruman, 3); + } + + @Test + // Author: alexander-novo + // A test to make sure the happy path of casting a second spell works + public void secondSpellTest() { + String bolt = "Lightning Bolt"; + + // Two spells to cast to trigger + addCard(Zone.HAND, playerA, saruman); + addCard(Zone.HAND, playerA, bolt); + addCard(Zone.LIBRARY, playerB, bolt); + + // Mana for casting spells + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + skipInitShuffling(); + + // Cast saruman, and then a second spell - make sure saruman triggers + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, saruman, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB); + checkStackObject("Bolt Check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "Whenever you cast your second spell each turn", 1); + + // Resolve the mill trigger - make sure the correct cards were milled and that the reflexive ability has triggered + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); + // I don't know why this is needed + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerB, true); + checkGraveyardCount("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerB, bolt, 1); + checkGraveyardCount("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Mountain", 1); + checkStackObject("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "When one or more cards are milled this way", 1); + + // Resolve the reflexive triggered ability - exiling the milled lightning bolt and casting it + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); + + // Choose player B to target for the next lightning bolt. Check to make sure there are now two lightning bolts on the stack + addTarget(playerA, playerB); + checkStackObject("Final check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + bolt, 2); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 2 * 3); + assertGraveyardCount(playerB, bolt, 0); + assertExileCount(playerB, bolt, 1); + } + + @Test + // Author: alexander-novo + // A test to make sure the mana value restriction works properly + public void manaValueTest() { + String bolt = "Lightning Bolt"; + String helix = "Lightning Helix"; + + // Two spells to cast to trigger + addCard(Zone.HAND, playerA, saruman); + addCard(Zone.HAND, playerA, bolt); + addCard(Zone.LIBRARY, playerB, helix); // This time, put a card we can't cast + + // Mana for casting spells + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + skipInitShuffling(); + + // Cast saruman, and then a second spell - make sure saruman triggers + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, saruman, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB); + checkStackObject("Bolt Check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "Whenever you cast your second spell each turn", 1); + + // Resolve the mill trigger - make sure the correct cards were milled and that the reflexive ability hasn't triggered because there are no targets + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); + // I don't know why this is needed + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerB, true); + checkGraveyardCount("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerB, helix, 1); + checkGraveyardCount("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Mountain", 1); + checkStackObject("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "When one or more cards are milled this way", 0); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 3); + assertGraveyardCount(playerB, helix, 1); + } + + @Test + // Author: alexander-novo + // A test to make sure the triggered ability still works if the original triggering spell is removed from the stack (such as by countering) + public void counterSpellTest() { + String bolt = "Lightning Bolt"; + String counter = "Counterspell"; + + // Two spells to cast to trigger. Counter to test LKI + addCard(Zone.HAND, playerA, saruman); + addCard(Zone.HAND, playerA, bolt); + addCard(Zone.HAND, playerB, counter); + addCard(Zone.LIBRARY, playerB, bolt); + + // Mana for casting spells + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + + skipInitShuffling(); + + // Cast saruman, and then a second spell - make sure saruman triggers + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, saruman, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB); + checkStackObject("Bolt Check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "Whenever you cast your second spell each turn", 1); + + // Counter the original lightning bolt cast. Everything else should work the same as if this hadn't happened + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, counter, bolt, bolt); + + // Resolve the mill trigger - make sure the correct cards were milled and that the reflexive ability has triggered + showStack("Counter check", 1, PhaseStep.PRECOMBAT_MAIN, playerB); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 2); + // I don't know why this is needed + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA, true); + checkGraveyardCount("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerB, bolt, 1); + checkGraveyardCount("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Mountain", 1); + checkStackObject("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "When one or more cards are milled this way", 1); + + // Resolve the reflexive triggered ability - exiling the milled lightning bolt and casting it + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); + + // Choose player B to target for the next lightning bolt. Check to make sure there are now two lightning bolts on the stack + addTarget(playerA, playerB); + checkStackObject("Final check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + bolt, 1); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + // Original lightning bolt was countered + assertLife(playerB, 20 - 3); + assertGraveyardCount(playerB, bolt, 0); + assertExileCount(playerB, bolt, 1); + } + + @Test + // Author: alexander-novo + // A test to make sure the reflexive trigger doesn't happen if there were no cards milled + public void noMillTest() { + String bolt = "Lightning Bolt"; + + // Two spells to cast to trigger + addCard(Zone.HAND, playerA, saruman); + addCard(Zone.HAND, playerA, bolt); + addCard(Zone.GRAVEYARD, playerB, bolt); + + // Mana for casting spells + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + removeAllCardsFromLibrary(playerB); + + // Cast saruman, and then a second spell - make sure saruman triggers + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, saruman, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB); + checkStackObject("Bolt Check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "Whenever you cast your second spell each turn", 1); + + // Resolve the mill trigger - make sure cards weren't milled, and the reflexive ability wasn't triggered because of it (even though there is a valid target in the lightning bolt) + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); + // I don't know why this is needed + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerB, true); + checkGraveyardCount("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerB, bolt, 1); + checkGraveyardCount("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Mountain", 0); + checkStackObject("Mill check", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "When one or more cards are milled this way", 0); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 3); + assertGraveyardCount(playerB, bolt, 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/ExileTargetCardCopyAndCastEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ExileTargetCardCopyAndCastEffect.java new file mode 100644 index 00000000000..0791dbd47a8 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/ExileTargetCardCopyAndCastEffect.java @@ -0,0 +1,70 @@ +package mage.abilities.effects.common; + +import mage.ApprovingObject; +import mage.abilities.Ability; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.players.Player; + +// Author: alexander-novo +// An effect for cards which instruct you to exile a card, then copy that card, and cast it. +// NOTE: You must set effect text on your own +public class ExileTargetCardCopyAndCastEffect extends OneShotEffect { + + private final boolean optional; + private final boolean noMana; + + public ExileTargetCardCopyAndCastEffect(boolean noMana) { + this(noMana, true); + } + + /** + * NOTE: You must supply your own effect text + * @param noMana Whether the copy can be cast without paying its mana cost + * @param optional Whether the casting of the copy is optional (otherwise it must be cast if possible) + */ + public ExileTargetCardCopyAndCastEffect(boolean noMana, boolean optional) { + super(Outcome.PlayForFree); + + this.optional = optional; + this.noMana = noMana; + } + + public ExileTargetCardCopyAndCastEffect(final ExileTargetCardCopyAndCastEffect effect) { + super(effect); + + this.optional = effect.optional; + this.noMana = effect.noMana; + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Card card = game.getCard(getTargetPointer().getFirst(game, source)); + if (player == null || card == null) { + return false; + } + player.moveCards(card, Zone.EXILED, source, game); + Card cardCopy = game.copyCard(card, source, source.getControllerId()); + if (optional && !player.chooseUse(outcome, "Cast copy of " + + card.getName() + " without paying its mana cost?", source, game)) { + return true; + } + game.getState().setValue("PlayFromNotOwnHandZone" + cardCopy.getId(), Boolean.TRUE); + player.cast( + player.chooseAbilityForCast(cardCopy, game, this.noMana), + game, this.noMana, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + cardCopy.getId(), null); + return true; + } + + @Override + public ExileTargetCardCopyAndCastEffect copy() { + return new ExileTargetCardCopyAndCastEffect(this); + } + +}