diff --git a/Mage.Sets/src/mage/cards/e/ErthaJoFrontierMentor.java b/Mage.Sets/src/mage/cards/e/ErthaJoFrontierMentor.java index 95662e76bff..cb1df4774ce 100644 --- a/Mage.Sets/src/mage/cards/e/ErthaJoFrontierMentor.java +++ b/Mage.Sets/src/mage/cards/e/ErthaJoFrontierMentor.java @@ -1,7 +1,6 @@ package mage.cards.e; import mage.MageInt; -import mage.MageObject; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.effects.common.CopyStackObjectEffect; @@ -12,17 +11,18 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; import mage.constants.Zone; +import mage.filter.FilterPlayer; import mage.filter.FilterStackObject; import mage.filter.StaticFilters; import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.filter.predicate.mageobject.TargetsPermanentOrPlayerPredicate; import mage.filter.predicate.mageobject.TargetsPermanentPredicate; import mage.filter.predicate.mageobject.TargetsPlayerPredicate; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.token.MercenaryToken; import mage.game.stack.StackObject; -import mage.target.common.TargetAnyTarget; import java.util.UUID; @@ -57,37 +57,16 @@ public final class ErthaJoFrontierMentor extends CardImpl { } } -// Some abstract type is confused there, so not using Predicates.or -enum ErthaJoFrontierMentorPredicate implements ObjectSourcePlayerPredicate { - instance; - - private static final TargetsPermanentPredicate permanentPredicate = - new TargetsPermanentPredicate(StaticFilters.FILTER_PERMANENT_CREATURE); - - private static final TargetsPlayerPredicate playerPredicate = new TargetsPlayerPredicate(); - - @Override - public boolean apply(ObjectSourcePlayer o, Game game) { - return permanentPredicate.apply(o, game) || playerPredicate.apply(o, game); - } - - @Override - public String toString() { - return "Or(" + permanentPredicate + ", " + playerPredicate + ')'; - } -} - class ErthaJoFrontierMentorTriggeredAbility extends TriggeredAbilityImpl { private static final FilterStackObject filter = new FilterStackObject("ability that targets a creature or player"); static { - filter.add(ErthaJoFrontierMentorPredicate.instance); + filter.add(new TargetsPermanentOrPlayerPredicate(StaticFilters.FILTER_PERMANENT_CREATURE, new FilterPlayer())); } public ErthaJoFrontierMentorTriggeredAbility() { super(Zone.BATTLEFIELD, new CopyStackObjectEffect(), false); - this.addTarget(new TargetAnyTarget()); setTriggerPhrase("Whenever you activate an ability that targets a creature or player, "); } diff --git a/Mage.Sets/src/mage/cards/o/Outwit.java b/Mage.Sets/src/mage/cards/o/Outwit.java index a3ca8264e09..46491d78ad6 100644 --- a/Mage.Sets/src/mage/cards/o/Outwit.java +++ b/Mage.Sets/src/mage/cards/o/Outwit.java @@ -1,42 +1,35 @@ package mage.cards.o; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; -import mage.abilities.Ability; import mage.abilities.effects.common.CounterTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Zone; -import mage.filter.Filter; +import mage.filter.FilterPlayer; import mage.filter.FilterSpell; -import mage.filter.StaticFilters; -import mage.game.Game; -import mage.game.stack.Spell; -import mage.game.stack.StackObject; -import mage.players.Player; -import mage.target.Target; -import mage.target.TargetObject; +import mage.filter.predicate.mageobject.TargetsPlayerPredicate; +import mage.target.TargetSpell; +import java.util.UUID; /** - * - * @author jeffwadsworth + * @author Susucr */ public final class Outwit extends CardImpl { private static FilterSpell filter = new FilterSpell("spell that targets a player"); - public Outwit(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{U}"); + static { + filter.add(new TargetsPlayerPredicate(new FilterPlayer())); + } + public Outwit(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{U}"); // Counter target spell that targets a player. this.getSpellAbility().addEffect(new CounterTargetEffect()); - this.getSpellAbility().addTarget(new CustomTargetSpell(filter)); + this.getSpellAbility().addTarget(new TargetSpell(filter)); } private Outwit(final Outwit card) { @@ -47,111 +40,4 @@ public final class Outwit extends CardImpl { public Outwit copy() { return new Outwit(this); } - - private static class CustomTargetSpell extends TargetObject { - - protected FilterSpell filter; - - public CustomTargetSpell() { - this(1, 1, StaticFilters.FILTER_SPELL); - } - - public CustomTargetSpell(FilterSpell filter) { - this(1, 1, filter); - } - - public CustomTargetSpell(int numTargets, FilterSpell filter) { - this(numTargets, numTargets, filter); - } - - public CustomTargetSpell(int minNumTargets, int maxNumTargets, FilterSpell filter) { - this.minNumberOfTargets = minNumTargets; - this.maxNumberOfTargets = maxNumTargets; - this.zone = Zone.STACK; - this.filter = filter; - this.targetName = filter.getMessage(); - } - - private CustomTargetSpell(final CustomTargetSpell target) { - super(target); - this.filter = target.filter.copy(); - } - - @Override - public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { - return canChoose(sourceControllerId, game); - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { - return possibleTargets(sourceControllerId, game); - } - - @Override - public boolean canTarget(UUID id, Ability source, Game game) { - if (super.canTarget(id, source, game)) { - if (targetsPlayer(id, game)) { - return true; - } - } - return false; - } - - @Override - public boolean canChoose(UUID sourceControllerId, Game game) { - int count = 0; - for (StackObject stackObject : game.getStack()) { - if (stackObject instanceof Spell && filter.match(stackObject, game)) { - if (targetsPlayer(stackObject.getId(), game)) { - count++; - if (count >= this.minNumberOfTargets) { - return true; - } - } - } - } - return false; - } - - @Override - public Set possibleTargets(UUID sourceControllerId, Game game) { - Set possibleTargets = new HashSet<>(); - for (StackObject stackObject : game.getStack()) { - if (stackObject instanceof Spell && filter.match(stackObject, game)) { - if (targetsPlayer(stackObject.getId(), game)) { - possibleTargets.add(stackObject.getId()); - } - } - } - return possibleTargets; - } - - @Override - public Filter getFilter() { - return filter; - } - - private boolean targetsPlayer(UUID id, Game game) { - StackObject spell = game.getStack().getStackObject(id); - if (spell != null) { - Ability ability = spell.getStackAbility(); - if (ability != null && !ability.getTargets().isEmpty()) { - for (Target target : ability.getTargets()) { - for (UUID playerId : target.getTargets()) { - Player player = game.getPlayer(playerId); - if (player != null) { - return true; - } - } - } - } - } - return false; - } - - @Override - public CustomTargetSpell copy() { - return new CustomTargetSpell(this); - } - } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/OutwitTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/OutwitTest.java new file mode 100644 index 00000000000..e74447912d6 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/OutwitTest.java @@ -0,0 +1,60 @@ +package org.mage.test.cards.single.avr; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class OutwitTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.o.Outwit Outwit} {U} + * Instant + * Counter target spell that targets a player. + */ + private static final String outwit = "Outwit"; + + @Test + public void test_BoltTargettingPlayer() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, "Island", 1); + addCard(Zone.HAND, playerB, outwit); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + checkPlayableAbility("Outwit castable", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Cast Outwit", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, outwit, "Lightning Bolt"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerB, 20); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertGraveyardCount(playerB, outwit, 1); + } + + @Test + public void test_BoltTargettingCreature_CantCastOutwit() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, "Island", 1); + addCard(Zone.HAND, playerB, outwit); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Memnite"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Memnite"); + checkPlayableAbility("Outwit not castable without valid target", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Cast Outwit", false); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Memnite", 1); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/ErthaJoFrontierMentorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/ErthaJoFrontierMentorTest.java new file mode 100644 index 00000000000..4769b5fac46 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/ErthaJoFrontierMentorTest.java @@ -0,0 +1,76 @@ +package org.mage.test.cards.single.otj; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class ErthaJoFrontierMentorTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.e.ErthaJoFrontierMentor Ertha Jo, Frontier Mentor} {2}{R}{W} + * Legendary Creature — Kor Advisor + * When Ertha Jo, Frontier Mentor enters the battlefield, create a 1/1 red Mercenary creature token with “{T}: Target creature you control gets +1/+0 until end of turn. Activate only as a sorcery.” + * Whenever you activate an ability that targets a creature or player, copy that ability. You may choose new targets for the copy. + * 2/4 + */ + private static final String ertha = "Ertha Jo, Frontier Mentor"; + + @Test + public void Test_TargetPlayer() { + setStrictChooseMode(true); + + // Sacrifice Bile Urchin: Target player loses 1 life. + addCard(Zone.BATTLEFIELD, playerA, "Bile Urchin", 1); + addCard(Zone.BATTLEFIELD, playerA, ertha, 1); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice", playerB); + setChoice(playerA, true); // choose to change the targets for the copy + addTarget(playerA, playerA); // have the copy target playerA + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 19); + assertLife(playerB, 19); + } + + @Test + public void Test_TargetCreature() { + setStrictChooseMode(true); + + // {T}: Target creature gets +1/+1 until end of turn. + addCard(Zone.BATTLEFIELD, playerA, "Wyluli Wolf", 1); + addCard(Zone.BATTLEFIELD, playerA, ertha, 1); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}", ertha); + setChoice(playerA, false); // choose not to change the targets for the copy + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPowerToughness(playerA, ertha, 2 + 2, 4 + 2); + } + + @Test + public void Test_TargetLand_NoCopy() { + setStrictChooseMode(true); + + // {2}{R}, {T}: Destroy target nonbasic land. + addCard(Zone.BATTLEFIELD, playerA, "Dwarven Miner", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.BATTLEFIELD, playerB, "Plateau", 1); + addCard(Zone.BATTLEFIELD, playerB, "Tropical Island", 1); + addCard(Zone.BATTLEFIELD, playerA, ertha, 1); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{R}", "Plateau"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerB, "Plateau", 1); + } +} diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPermanentOrPlayerPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPermanentOrPlayerPredicate.java new file mode 100644 index 00000000000..73a7a222840 --- /dev/null +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPermanentOrPlayerPredicate.java @@ -0,0 +1,61 @@ +package mage.filter.predicate.mageobject; + +import mage.abilities.Mode; +import mage.filter.FilterPermanent; +import mage.filter.FilterPlayer; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.stack.StackObject; +import mage.players.Player; +import mage.target.Target; + +import java.util.UUID; + +/** + * @author Susucr + */ +public class TargetsPermanentOrPlayerPredicate implements ObjectSourcePlayerPredicate { + + private final FilterPermanent targetFilterPermanent; + private final FilterPlayer targetFilterPlayer; + + public TargetsPermanentOrPlayerPredicate(FilterPermanent targetFilterPermanent, FilterPlayer targetFilterPlayer) { + this.targetFilterPermanent = targetFilterPermanent; + this.targetFilterPlayer = targetFilterPlayer; + } + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + StackObject object = game.getStack().getStackObject(input.getObject().getId()); + if (object != null) { + for (UUID modeId : object.getStackAbility().getModes().getSelectedModes()) { + Mode mode = object.getStackAbility().getModes().get(modeId); + for (Target target : mode.getTargets()) { + if (target.isNotTarget()) { + continue; + } + for (UUID targetId : target.getTargets()) { + // Try for permanent + Permanent permanent = game.getPermanent(targetId); + if (targetFilterPermanent.match(permanent, input.getPlayerId(), input.getSource(), game)) { + return true; + } + // Try for player + Player player = game.getPlayer(targetId); + if (targetFilterPlayer.match(player, input.getPlayerId(), input.getSource(), game)) { + return true; + } + } + } + } + } + return false; + } + + @Override + public String toString() { + return "that targets a " + targetFilterPermanent.getMessage() + " or " + targetFilterPlayer.getMessage(); + } +} diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPermanentPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPermanentPredicate.java index 7d2f9fe9dca..a116ab23cf1 100644 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPermanentPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPermanentPredicate.java @@ -1,6 +1,5 @@ package mage.filter.predicate.mageobject; -import mage.MageObject; import mage.abilities.Mode; import mage.filter.FilterPermanent; import mage.filter.predicate.ObjectSourcePlayer; @@ -15,7 +14,7 @@ import java.util.UUID; /** * @author LoneFox */ -public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate { +public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate { private final FilterPermanent targetFilter; @@ -24,7 +23,7 @@ public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { StackObject object = game.getStack().getStackObject(input.getObject().getId()); if (object != null) { for (UUID modeId : object.getStackAbility().getModes().getSelectedModes()) { diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPlayerPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPlayerPredicate.java index d5af2774f44..6dbe8838245 100644 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPlayerPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/TargetsPlayerPredicate.java @@ -1,8 +1,7 @@ package mage.filter.predicate.mageobject; -import java.util.UUID; -import mage.MageObject; import mage.abilities.Mode; +import mage.filter.FilterPlayer; import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; @@ -10,25 +9,34 @@ import mage.game.stack.StackObject; import mage.players.Player; import mage.target.Target; -/** - * - * @author jeffwadsworth - */ -public class TargetsPlayerPredicate implements ObjectSourcePlayerPredicate { +import java.util.UUID; - public TargetsPlayerPredicate() { +/** + * @author jeffwadsworth, Susucr + */ +public class TargetsPlayerPredicate implements ObjectSourcePlayerPredicate { + + private final FilterPlayer targetFilter; + + public TargetsPlayerPredicate(FilterPlayer targetFilter) { + this.targetFilter = targetFilter; } @Override - public boolean apply(ObjectSourcePlayer input, Game game) { + public boolean apply(ObjectSourcePlayer input, Game game) { StackObject object = game.getStack().getStackObject(input.getObject().getId()); if (object != null) { for (UUID modeId : object.getStackAbility().getModes().getSelectedModes()) { Mode mode = object.getStackAbility().getModes().get(modeId); for (Target target : mode.getTargets()) { + if (target.isNotTarget()) { + continue; + } for (UUID targetId : target.getTargets()) { Player player = game.getPlayer(targetId); - return player != null; + if (targetFilter.match(player, input.getPlayerId(), input.getSource(), game)) { + return true; + } } } } @@ -38,6 +46,6 @@ public class TargetsPlayerPredicate implements ObjectSourcePlayerPredicate