Added Saruman of Many Colors (#10434)

* Added Saruman of Many Colors

Borrowed functionality in CastSecondSpellTriggeredAbility from magefree/mage#10433
Added new helper class ExileTargetCardCopyAndCastEffect for common effect

* Updated FlawlessForgery to use new ExileTargetCardCopyAndCastEffect

* Fixed overriden Effect copy functions

* Expanded ExileTargetCardCopyAndCastEffect

Added ability for non-free spells

* Removed filter lock

* De-duplicated exile and cast effects

* Fixed demilich
This commit is contained in:
Alexander Novotny 2023-06-08 14:00:28 -07:00 committed by GitHub
parent 0b2f582d84
commit 80cb439862
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 454 additions and 358 deletions

View file

@ -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<Card> {
.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;
}
}
}

View file

@ -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() {

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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() {

View file

@ -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<Card
.map(p -> 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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<Card> 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);
// WardDiscard 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);
}
}

View file

@ -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));

View file

@ -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);
}
}

View file

@ -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);
}
}