diff --git a/Mage.Sets/src/mage/cards/f/FallenShinobi.java b/Mage.Sets/src/mage/cards/f/FallenShinobi.java index 7b6f873d293..1cac01b1244 100644 --- a/Mage.Sets/src/mage/cards/f/FallenShinobi.java +++ b/Mage.Sets/src/mage/cards/f/FallenShinobi.java @@ -5,14 +5,15 @@ import mage.abilities.Ability; import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; import mage.abilities.keyword.NinjutsuAbility; -import mage.cards.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.Game; import mage.players.Player; import java.util.UUID; -import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; /** * @author TheElk801 @@ -69,7 +70,7 @@ class FallenShinobiEffect extends OneShotEffect { if (player == null) { return false; } - return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, player.getLibrary().getTopCards(game, 2), - TargetController.YOU, Duration.EndOfTurn, true); + return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, player.getLibrary().getTopCards(game, 2), + TargetController.YOU, Duration.EndOfTurn, true, false); } } diff --git a/Mage.Sets/src/mage/cards/f/FuriousRise.java b/Mage.Sets/src/mage/cards/f/FuriousRise.java index 0ef4796198d..fd275be0f81 100644 --- a/Mage.Sets/src/mage/cards/f/FuriousRise.java +++ b/Mage.Sets/src/mage/cards/f/FuriousRise.java @@ -1,7 +1,5 @@ package mage.cards.f; -import java.util.List; -import java.util.UUID; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.BeginningOfEndStepTriggeredAbility; @@ -14,18 +12,16 @@ import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.AsThoughEffectType; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.TargetController; +import mage.constants.*; import mage.game.Game; import mage.players.Player; import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; +import java.util.List; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class FuriousRise extends CardImpl { @@ -77,7 +73,7 @@ class FuriousRiseEffect extends OneShotEffect { controller.moveCardsToExile(cardToExile, source, game, true, exileId, mageObject.getIdName() + " (" + source.getSourceObjectZoneChangeCounter() + ")"); Card cardToPlay = game.getCard(cardToExile.getId()); - endPreviousEffect(game, source); + endPreviousEffect(game, source); // workaround for Furious Rise ContinuousEffect effect = new FuriousRisePlayEffect(); effect.setTargetPointer(new FixedTarget(cardToPlay, game)); @@ -93,7 +89,7 @@ class FuriousRiseEffect extends OneShotEffect { for (Ability ability : game.getContinuousEffects().getAsThoughEffectsAbility(effect)) { if (ability.getSourceId().equals(source.getSourceId()) && source.getSourceObjectZoneChangeCounter() == ability.getSourceObjectZoneChangeCounter()) { - ((FuriousRisePlayEffect) effect).discard(); + effect.discard(); return true; } } diff --git a/Mage.Sets/src/mage/cards/g/GolosTirelessPilgrim.java b/Mage.Sets/src/mage/cards/g/GolosTirelessPilgrim.java index abd9d198932..b448c00551f 100644 --- a/Mage.Sets/src/mage/cards/g/GolosTirelessPilgrim.java +++ b/Mage.Sets/src/mage/cards/g/GolosTirelessPilgrim.java @@ -1,7 +1,5 @@ package mage.cards.g; -import java.util.Set; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; @@ -19,6 +17,9 @@ import mage.game.Game; import mage.players.Player; import mage.target.common.TargetCardInLibrary; +import java.util.Set; +import java.util.UUID; + /** * @author TheElk801 */ @@ -74,7 +75,7 @@ class GolosTirelessPilgrimEffect extends OneShotEffect { } Set cards = player.getLibrary().getTopCards(game, 3); return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, cards, - TargetController.YOU, Duration.EndOfTurn, true); + TargetController.YOU, Duration.EndOfTurn, true, false); } @Override diff --git a/Mage.Sets/src/mage/cards/i/IgniteTheFuture.java b/Mage.Sets/src/mage/cards/i/IgniteTheFuture.java index d42e4f936d4..828ada18fa8 100644 --- a/Mage.Sets/src/mage/cards/i/IgniteTheFuture.java +++ b/Mage.Sets/src/mage/cards/i/IgniteTheFuture.java @@ -3,6 +3,7 @@ package mage.cards.i; import mage.abilities.Ability; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; import mage.abilities.keyword.FlashbackAbility; import mage.cards.Card; import mage.cards.CardImpl; @@ -14,7 +15,6 @@ import mage.players.Player; import java.util.Set; import java.util.UUID; -import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; /** * @author TheElk801 @@ -68,7 +68,7 @@ class IgniteTheFutureEffect extends OneShotEffect { return false; } Set cards = controller.getLibrary().getTopCards(game, 3); - return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, cards, - TargetController.YOU, Duration.UntilEndOfYourNextTurn, Zone.GRAVEYARD.equals(spell.getFromZone())); + return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, cards, + TargetController.YOU, Duration.UntilEndOfYourNextTurn, Zone.GRAVEYARD.equals(spell.getFromZone()), false); } } diff --git a/Mage.Sets/src/mage/cards/k/KheruSpellsnatcher.java b/Mage.Sets/src/mage/cards/k/KheruSpellsnatcher.java index 5c4bb02b2ac..4bfea73de60 100644 --- a/Mage.Sets/src/mage/cards/k/KheruSpellsnatcher.java +++ b/Mage.Sets/src/mage/cards/k/KheruSpellsnatcher.java @@ -6,6 +6,7 @@ import mage.abilities.Ability; import mage.abilities.common.TurnedFaceUpSourceTriggeredAbility; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; import mage.abilities.keyword.MorphAbility; import mage.cards.Card; import mage.cards.CardImpl; @@ -16,7 +17,6 @@ import mage.game.stack.StackObject; import mage.target.TargetSpell; import java.util.UUID; -import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; /** * @author emerald000 @@ -76,8 +76,9 @@ class KheruSpellsnatcherEffect extends OneShotEffect { && game.getStack().counter(targetPointer.getFirst(game, source), source, game, Zone.EXILED, false, ZoneDetail.NONE)) { if (!stackObject.isCopy()) { MageObject card = game.getObject(stackObject.getSourceId()); - if (card instanceof Card) { - return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, (Card)card, TargetController.YOU, Duration.Custom, true); + if (card instanceof Card) { + return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, (Card) card, + TargetController.YOU, Duration.Custom, true, true); } } } diff --git a/Mage.Sets/src/mage/cards/m/MagusOfTheMind.java b/Mage.Sets/src/mage/cards/m/MagusOfTheMind.java index c3aa993fd2a..50d46d6f5fa 100644 --- a/Mage.Sets/src/mage/cards/m/MagusOfTheMind.java +++ b/Mage.Sets/src/mage/cards/m/MagusOfTheMind.java @@ -1,7 +1,5 @@ package mage.cards.m; -import java.util.Set; -import java.util.UUID; import mage.MageInt; import mage.MageObject; import mage.abilities.Ability; @@ -14,18 +12,15 @@ import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffec import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.TargetController; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Game; import mage.players.Player; import mage.watchers.common.CastSpellLastTurnWatcher; +import java.util.Set; +import java.util.UUID; + /** - * * @author TheElk801 */ public final class MagusOfTheMind extends CardImpl { @@ -84,7 +79,8 @@ class MagusOfTheMindEffect extends OneShotEffect { controller.shuffleLibrary(source, game); if (controller.getLibrary().hasCards()) { Set cards = controller.getLibrary().getTopCards(game, stormCount); - return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, cards, TargetController.YOU, Duration.EndOfTurn, true); + return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, cards, + TargetController.YOU, Duration.EndOfTurn, true, false); } return true; } diff --git a/Mage.Sets/src/mage/cards/m/MindsDesire.java b/Mage.Sets/src/mage/cards/m/MindsDesire.java index d5cf67afd69..f15f1d24e6d 100644 --- a/Mage.Sets/src/mage/cards/m/MindsDesire.java +++ b/Mage.Sets/src/mage/cards/m/MindsDesire.java @@ -2,18 +2,18 @@ package mage.cards.m; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; import mage.abilities.keyword.StormAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; import mage.constants.Outcome; +import mage.constants.TargetController; import mage.game.Game; import mage.players.Player; import java.util.UUID; -import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; -import mage.constants.TargetController; /** * @author emerald000 @@ -62,7 +62,7 @@ class MindsDesireEffect extends OneShotEffect { if (controller != null) { controller.shuffleLibrary(source, game); return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, controller.getLibrary().getFromTop(game), - TargetController.YOU, Duration.EndOfTurn, true); + TargetController.YOU, Duration.EndOfTurn, true, false); } return false; } diff --git a/Mage.Sets/src/mage/cards/o/OraclesVault.java b/Mage.Sets/src/mage/cards/o/OraclesVault.java index 120661b536f..49621bb192b 100644 --- a/Mage.Sets/src/mage/cards/o/OraclesVault.java +++ b/Mage.Sets/src/mage/cards/o/OraclesVault.java @@ -1,6 +1,5 @@ package mage.cards.o; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.condition.common.SourceHasCounterCondition; @@ -13,17 +12,14 @@ import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffec import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.TargetController; -import mage.constants.Zone; +import mage.constants.*; import mage.counters.CounterType; import mage.game.Game; import mage.players.Player; +import java.util.UUID; + /** - * * @author fireshoes */ public final class OraclesVault extends CardImpl { @@ -46,7 +42,7 @@ public final class OraclesVault extends CardImpl { this.addAbility(new ConditionalActivatedAbility(Zone.BATTLEFIELD, new OraclesVaultFreeEffect(), new TapSourceCost(), new SourceHasCounterCondition(CounterType.BRICK, 3, Integer.MAX_VALUE), "{T}: Exile the top card of your library. Until end of turn, you may play that card without paying its mana cost. " - + "Activate this ability only if there are three or more brick counters on {this}")); + + "Activate this ability only if there are three or more brick counters on {this}")); } public OraclesVault(final OraclesVault card) { @@ -79,7 +75,7 @@ class OraclesVaultEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, controller.getLibrary().getFromTop(game), - TargetController.YOU, Duration.EndOfTurn, false); + TargetController.YOU, Duration.EndOfTurn, false, false); } return false; } @@ -105,7 +101,7 @@ class OraclesVaultFreeEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, controller.getLibrary().getFromTop(game), - TargetController.YOU, Duration.EndOfTurn, true); + TargetController.YOU, Duration.EndOfTurn, true, false); } return false; } diff --git a/Mage.Sets/src/mage/cards/r/ReleaseToTheWind.java b/Mage.Sets/src/mage/cards/r/ReleaseToTheWind.java index cf4d704047b..eb61e77e06b 100644 --- a/Mage.Sets/src/mage/cards/r/ReleaseToTheWind.java +++ b/Mage.Sets/src/mage/cards/r/ReleaseToTheWind.java @@ -1,7 +1,5 @@ - package mage.cards.r; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; @@ -16,8 +14,9 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetNonlandPermanent; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class ReleaseToTheWind extends CardImpl { @@ -28,7 +27,6 @@ public final class ReleaseToTheWind extends CardImpl { // Exile target nonland permanent. For as long as that card remains exiled, its owner may cast it without paying its mana cost. getSpellAbility().addEffect(new ReleaseToTheWindEffect()); getSpellAbility().addTarget(new TargetNonlandPermanent()); - } public ReleaseToTheWind(final ReleaseToTheWind card) { @@ -63,7 +61,8 @@ class ReleaseToTheWindEffect extends OneShotEffect { if (controller != null) { Permanent targetPermanent = game.getPermanent(getTargetPointer().getFirst(game, source)); if (targetPermanent != null) { - return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, targetPermanent, TargetController.OWNER, Duration.Custom, true); + return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, targetPermanent, + TargetController.OWNER, Duration.Custom, true, true); } } return false; diff --git a/Mage.Sets/src/mage/cards/t/Tromokratis.java b/Mage.Sets/src/mage/cards/t/Tromokratis.java index 9590fd92d39..bf6b0fde585 100644 --- a/Mage.Sets/src/mage/cards/t/Tromokratis.java +++ b/Mage.Sets/src/mage/cards/t/Tromokratis.java @@ -81,7 +81,7 @@ class CantBeBlockedUnlessAllEffect extends RestrictionEffect { // check if all creatures of defender are able to block this permanent // permanent.canBlock() can't be used because causing recursive call for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, blocker.getControllerId(), game)) { - if (permanent.isTapped() && null == game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, source, blocker.getControllerId(), game)) { + if (permanent.isTapped() && null == game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, blocker.getControllerId(), game)) { return false; } // check blocker restrictions diff --git a/Mage.Sets/src/mage/cards/u/UrzaLordHighArtificer.java b/Mage.Sets/src/mage/cards/u/UrzaLordHighArtificer.java index 0599b62e1e4..7918713fdd5 100644 --- a/Mage.Sets/src/mage/cards/u/UrzaLordHighArtificer.java +++ b/Mage.Sets/src/mage/cards/u/UrzaLordHighArtificer.java @@ -1,7 +1,5 @@ package mage.cards.u; -import java.util.ArrayList; -import java.util.List; import mage.MageInt; import mage.MageObject; import mage.Mana; @@ -13,12 +11,14 @@ import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; +import mage.abilities.effects.mana.BasicManaEffect; import mage.abilities.hint.common.ArtifactYouControlHint; import mage.abilities.mana.SimpleManaAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; +import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledArtifactPermanent; import mage.filter.common.FilterControlledPermanent; import mage.filter.predicate.Predicates; @@ -28,9 +28,9 @@ import mage.game.permanent.token.KarnConstructToken; import mage.players.Player; import mage.target.common.TargetControlledPermanent; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; -import mage.abilities.effects.mana.BasicManaEffect; -import mage.filter.FilterPermanent; /** * @author TheElk801 @@ -55,7 +55,7 @@ public final class UrzaLordHighArtificer extends CardImpl { FilterControlledPermanent filter = new FilterControlledArtifactPermanent("untapped artifact you control"); filter.add(Predicates.not(TappedPredicate.instance)); this.addAbility(new SimpleManaAbility( - Zone.BATTLEFIELD, + Zone.BATTLEFIELD, new UrzaLordHighArtificerManaEffect(filter), new TapTargetCost(new TargetControlledPermanent(filter)))); @@ -100,14 +100,14 @@ class UrzaLordHighArtificerEffect extends OneShotEffect { controller.shuffleLibrary(source, game); Card card = controller.getLibrary().getFromTop(game); return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, card, - TargetController.YOU, Duration.EndOfTurn, true); + TargetController.YOU, Duration.EndOfTurn, true, false); } } class UrzaLordHighArtificerManaEffect extends BasicManaEffect { private final FilterPermanent filter; - + public UrzaLordHighArtificerManaEffect(FilterPermanent filter) { super(Mana.BlueMana(1)); this.filter = filter; @@ -131,7 +131,7 @@ class UrzaLordHighArtificerManaEffect extends BasicManaEffect { if (count > 0) { netMana.add(Mana.BlueMana(count)); } - return netMana; + return netMana; } return super.getNetMana(game, source); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/asthough/PlayFromNonHandZoneTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/asthough/PlayFromNonHandZoneTest.java index 7a7aa3f7b91..89a89938bea 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/asthough/PlayFromNonHandZoneTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/asthough/PlayFromNonHandZoneTest.java @@ -437,12 +437,18 @@ public class PlayFromNonHandZoneTest extends CardTestPlayerBaseWithAIHelps { skipInitShuffling(); + // attack and exile 2 cards from library attack(2, playerB, "Fallen Shinobi"); + checkExileCount("after exile a", 2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Angelic Purge", 1); + checkExileCount("after exile b", 2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Curious Pair", 1); + // cast purge from exile + checkPlayableAbility("after exile - can play purge", 2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Cast Angelic Purge", true); castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Angelic Purge"); - setChoice(playerB, "Silvercoat Lion"); // Sacrifice for Purge - addTarget(playerB, "Amulet of Kroog"); // Exile with Purge + setChoice(playerB, "Silvercoat Lion"); // sacrifice cost + addTarget(playerB, "Amulet of Kroog"); // exile target + // cast adventure spell from exile castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Treats to Share"); setStopAt(2, PhaseStep.END_TURN); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java index 8859f44a2a5..80ea599f655 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java @@ -238,8 +238,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase { waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); checkGraveyardCount("after grave a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 0); checkGraveyardCount("after grave b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears", 0); - checkExileCount("after exile a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1); - checkExileCount("after exile b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears", 0); + checkExileCount("after exile", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1); // B can cast green bear for red mana castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/rix/ReleaseToTheWindTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/rix/ReleaseToTheWindTest.java new file mode 100644 index 00000000000..fc0f082765e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/rix/ReleaseToTheWindTest.java @@ -0,0 +1,76 @@ +package org.mage.test.cards.single.rix; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class ReleaseToTheWindTest extends CardTestPlayerBase { + + @Test + public void test_Exile_PermanentCard() { + // Exile target nonland permanent. For as long as that card remains exiled, its owner may cast it without paying its mana cost. + addCard(Zone.HAND, playerA, "Release to the Wind"); // {2}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + // + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1); + + // exile + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Release to the Wind", "Grizzly Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkExileCount("after exile", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Grizzly Bears", 1); + checkPlayableAbility("after exile - non owner can't play 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Grizzly Bears", false); + + // owner can play + checkPlayableAbility("after exile - non owner can't play 2", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Grizzly Bears", false); + checkPlayableAbility("after exile - owner can play", 2, PhaseStep.PRECOMBAT_MAIN, playerB, "Cast Grizzly Bears", true); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Grizzly Bears"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerB, "Grizzly Bears", 1); + } + + @Test + public void test_Exile_ModalDoubleFacesCard() { + // Exile target nonland permanent. For as long as that card remains exiled, its owner may cast it without paying its mana cost. + addCard(Zone.HAND, playerA, "Release to the Wind"); // {2}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + // + // Akoum Warrior {5}{R} - creature + // Akoum Teeth - land + addCard(Zone.HAND, playerA, "Akoum Warrior"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + // prepare mdf + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 6); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akoum Warrior"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + checkPermanentCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akoum Warrior", 1); + + // exile mdf creature + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Release to the Wind", "Akoum Warrior"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkExileCount("after exile", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akoum Warrior", 1); + + // you can cast mdf, but can't play a land + checkPlayableAbility("after exile - can play mdf creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Akoum Warrior", true); + checkPlayableAbility("after exile - can't play mdf land", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Akoum Teeth", false); + + // cast mdf again for free + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akoum Warrior"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Akoum Warrior", 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 17c3b0553ba..fd318f7cb11 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -820,7 +820,7 @@ public class TestPlayer implements Player { // check exile count: card name, count if (params[0].equals(CHECK_COMMAND_EXILE_COUNT) && params.length == 3) { - assertExileCount(action, game, computerPlayer, params[1], Integer.parseInt(params[2])); + assertExileCount(action, game, params[1], Integer.parseInt(params[2])); actions.remove(action); wasProccessed = true; } @@ -1355,15 +1355,20 @@ public class TestPlayer implements Player { Assert.assertEquals(action.getActionName() + " - permanent " + permanentName + " must have " + count + " " + counterType.toString(), count, foundCount); } - private void assertExileCount(PlayerAction action, Game game, Player player, String permanentName, int count) { + private void assertExileCount(PlayerAction action, Game game, String permanentName, int count) { int foundCount = 0; for (Card card : game.getExile().getAllCards(game)) { - if (hasObjectTargetNameOrAlias(card, permanentName) && card.isOwnedBy(player.getId())) { + if (hasObjectTargetNameOrAlias(card, permanentName)) { foundCount++; } } - Assert.assertEquals(action.getActionName() + " - card " + permanentName + " must exists in exile zone with " + count + " instances", count, foundCount); + if (foundCount != count) { + printStart("Exile cards"); + printCards(game.getExile().getAllCards(game)); + printEnd(); + Assert.fail(action.getActionName() + " - exile zone must have " + count + " cards with name " + permanentName + ", but found " + foundCount); + } } private void assertGraveyardCount(PlayerAction action, Game game, Player player, String permanentName, int count) { @@ -1374,7 +1379,12 @@ public class TestPlayer implements Player { } } - Assert.assertEquals(action.getActionName() + " - card " + permanentName + " must exists in graveyard zone with " + count + " instances", count, foundCount); + if (foundCount != count) { + printStart("Graveyard of " + player.getName()); + printCards(player.getGraveyard().getCards(game)); + printEnd(); + Assert.fail(action.getActionName() + " - graveyard zone must have " + count + " cards with name " + permanentName + ", but found " + foundCount); + } } private void assertLibraryCount(PlayerAction action, Game game, Player player, String permanentName, int count) { diff --git a/Mage/src/main/java/mage/abilities/PlayLandAbility.java b/Mage/src/main/java/mage/abilities/PlayLandAbility.java index 8fa143d8fc9..0b8c3df6167 100644 --- a/Mage/src/main/java/mage/abilities/PlayLandAbility.java +++ b/Mage/src/main/java/mage/abilities/PlayLandAbility.java @@ -1,14 +1,14 @@ package mage.abilities; -import java.util.UUID; import mage.ApprovingObject; import mage.constants.AbilityType; import mage.constants.AsThoughEffectType; import mage.constants.Zone; import mage.game.Game; +import java.util.UUID; + /** - * * @author BetaSteward_at_googlemail.com */ public class PlayLandAbility extends ActivatedAbilityImpl { @@ -25,7 +25,7 @@ public class PlayLandAbility extends ActivatedAbilityImpl { @Override public ActivationStatus canActivate(UUID playerId, Game game) { - ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, null, playerId, game); + ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); if (!controlsAbility(playerId, game) && null == approvingObject) { return ActivationStatus.getFalse(); } diff --git a/Mage/src/main/java/mage/abilities/SpellAbility.java b/Mage/src/main/java/mage/abilities/SpellAbility.java index 20428c4018d..c44baebfe6c 100644 --- a/Mage/src/main/java/mage/abilities/SpellAbility.java +++ b/Mage/src/main/java/mage/abilities/SpellAbility.java @@ -1,5 +1,6 @@ package mage.abilities; +import mage.ApprovingObject; import mage.MageObject; import mage.abilities.costs.Cost; import mage.abilities.costs.VariableCost; @@ -15,7 +16,6 @@ import mage.players.Player; import java.util.Optional; import java.util.UUID; -import mage.ApprovingObject; /** * @author BetaSteward_at_googlemail.com @@ -81,14 +81,17 @@ public class SpellAbility extends ActivatedAbilityImpl { || spellAbilityType == SpellAbilityType.SPLIT_AFTERMATH) { return ActivationStatus.getFalse(); } - // fix for Gitaxian Probe and casting opponent's spells - ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, null, playerId, game); + + // play from not own hand + ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); if (approvingObject == null) { Card card = game.getCard(sourceId); if (!(card != null && card.isOwnedBy(playerId))) { return ActivationStatus.getFalse(); } } + + // play restrict // Check if rule modifying events prevent to cast the spell in check playable mode if (game.inCheckPlayableState()) { if (game.getContinuousEffects().preventedByRuleModification( @@ -96,6 +99,8 @@ public class SpellAbility extends ActivatedAbilityImpl { return ActivationStatus.getFalse(); } } + + // no mana restrict // Alternate spell abilities (Flashback, Overload) can't be cast with no mana to pay option if (getSpellAbilityType() == SpellAbilityType.BASE_ALTERNATE) { Player player = game.getPlayer(playerId); @@ -104,6 +109,8 @@ public class SpellAbility extends ActivatedAbilityImpl { return ActivationStatus.getFalse(); } } + + // can pay all costs if (costs.canPay(this, this, playerId, game)) { if (getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) { SplitCard splitCard = (SplitCard) game.getCard(getSourceId()); @@ -116,7 +123,6 @@ public class SpellAbility extends ActivatedAbilityImpl { } } return ActivationStatus.getFalse(); - } else { return new ActivationStatus(canChooseTarget(game), approvingObject); } diff --git a/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java b/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java index 29333282978..bc4d6d4f6c2 100644 --- a/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java @@ -1,25 +1,45 @@ - package mage.abilities.effects; -import java.util.UUID; import mage.abilities.Ability; import mage.constants.AsThoughEffectType; import mage.game.Game; +import java.util.UUID; + /** - * * @author BetaSteward_at_googlemail.com */ public interface AsThoughEffect extends ContinuousEffect { + /** + * Apply to ONE affected ability from the object (sourceId) + *

+ * Warning, if you don't need ability to check then ignore it (by default it calls full object check) + * + * @param sourceId + * @param affectedAbility ability to check (example: check if spell ability can be cast from non hand) + * @param source + * @param game + * @param playerId player to check + * @return + */ boolean applies(UUID sourceId, Ability affectedAbility, Ability source, Game game, UUID playerId); + /** + * Apply to ANY ability from the object (sourceId) + * + * @param sourceId object to check + * @param source + * @param affectedControllerId player to check (example: you can activate opponent's card or ability) + * @param game + * @return + */ boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game); AsThoughEffectType getAsThoughEffectType(); @Override AsThoughEffect copy(); - + boolean isConsumable(); } diff --git a/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java index cb3dba2f6c9..8f98c2c8793 100644 --- a/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java @@ -38,12 +38,10 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements @Override public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { - // affectedControllerId = player to check - if (getAsThoughEffectType().equals(AsThoughEffectType.LOOK_AT_FACE_DOWN)) { - return applies(objectId, source, playerId, game); - } else { - return applies(objectId, source, playerId, game); - } + // affectedControllerId = player to check (example: you can activate ability from opponent's card) + // by default it applies to full object + // if you AsThough effect type needs affected ability then override that method + return applies(objectId, source, playerId, game); } @Override diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index b6ae54aff7e..7b09409d484 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -1,9 +1,5 @@ package mage.abilities.effects; -import java.io.Serializable; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; import mage.ApprovingObject; import mage.MageObject; import mage.abilities.Ability; @@ -32,6 +28,11 @@ import mage.util.CardUtil; import mage.util.trace.TraceInfo; import org.apache.log4j.Logger; +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + /** * @author BetaSteward_at_googlemail.com */ @@ -500,22 +501,43 @@ public class ContinuousEffects implements Serializable { } /** - * @param objectId + * @param objectId object to check * @param type - * @param affectedAbility + * @param affectedAbility null if check full object or ability if check only one ability from that object * @param controllerId * @param game - * @return sourceId of the permitting effect if any exists otherwise returns - * null + * @return sourceId of the permitting effect if any exists otherwise returns null */ public ApprovingObject asThough(UUID objectId, AsThoughEffectType type, Ability affectedAbility, UUID controllerId, Game game) { + + // usage check: effect must apply for specific ability only, not to full object (example: PLAY_FROM_NOT_OWN_HAND_ZONE) + if (type.needAffectedAbility() && affectedAbility == null) { + throw new IllegalArgumentException("ERROR, you can't call asThough check to whole object, call it with affected ability instead: " + type); + } + + // usage check: effect must apply to full object, not specific ability (example: ATTACK_AS_HASTE) + // P.S. In theory a same AsThough effect can be applied to object or to ability, so if you really, really + // need it then disable that check or add extra param to AsThoughEffectType like needAffectedAbilityOrFullObject + if (!type.needAffectedAbility() && affectedAbility != null) { + throw new IllegalArgumentException("ERROR, you can't call AsThough check to affected ability, call it empty affected ability instead: " + type); + } + List asThoughEffectsList = getApplicableAsThoughEffects(type, game); if (!asThoughEffectsList.isEmpty()) { + MageObject objectToCheck; + if (affectedAbility != null) { + objectToCheck = affectedAbility.getSourceObject(game); + } else { + objectToCheck = game.getCard(objectId); + } + UUID idToCheck; - if (affectedAbility != null && affectedAbility.getSourceObject(game) instanceof SplitCardHalf) { - idToCheck = ((SplitCardHalf) affectedAbility.getSourceObject(game)).getParentCard().getId(); - } else if (affectedAbility != null && affectedAbility.getSourceObject(game) instanceof ModalDoubleFacesCardHalf - && !type.needPlayCardAbility()) { + if (objectToCheck instanceof SplitCardHalf) { + idToCheck = ((SplitCardHalf) objectToCheck).getMainCard().getId(); + } else if (!type.needPlayCardAbility() && objectToCheck instanceof AdventureCardSpell) { + // adventure spell uses alternative characteristics for spell/stack, all other cases must use main card + idToCheck = ((AdventureCardSpell) objectToCheck).getMainCard().getId(); + } else if (!type.needPlayCardAbility() && objectToCheck instanceof ModalDoubleFacesCardHalf) { // each mdf side uses own characteristics to check for playing, all other cases must use main card // rules: // "If an effect allows you to play a land or cast a spell from among a group of cards, @@ -523,26 +545,9 @@ public class ContinuousEffects implements Serializable { // of that effect. For example, if Sejiri Shelter / Sejiri Glacier is in your graveyard // and an effect allows you to play lands from your graveyard, you could play Sejiri Glacier. // That effect doesn't allow you to cast Sejiri Shelter." - idToCheck = ((ModalDoubleFacesCardHalf) affectedAbility.getSourceObject(game)).getParentCard().getId(); - } else if (affectedAbility != null && affectedAbility.getSourceObject(game) instanceof AdventureCardSpell - && !type.needPlayCardAbility()) { - // adventure spell uses alternative characteristics for spell/stack - idToCheck = ((AdventureCardSpell) affectedAbility.getSourceObject(game)).getParentCard().getId(); + idToCheck = ((ModalDoubleFacesCardHalf) objectToCheck).getMainCard().getId(); } else { - Card card = game.getCard(objectId); - if (card instanceof SplitCardHalf) { - idToCheck = ((SplitCardHalf) card).getParentCard().getId(); - } else if (card instanceof ModalDoubleFacesCardHalf - && !type.needPlayCardAbility()) { - // each mdf side uses own characteristics to check for playing, all other cases must use main card - idToCheck = ((ModalDoubleFacesCardHalf) card).getParentCard().getId(); - } else if (card instanceof AdventureCardSpell - && !type.needPlayCardAbility()) { - // adventure spell uses alternative characteristics for spell/stack - idToCheck = ((AdventureCardSpell) card).getParentCard().getId(); - } else { - idToCheck = objectId; - } + idToCheck = objectId; } Set possibleApprovingObjects = new HashSet<>(); @@ -550,7 +555,7 @@ public class ContinuousEffects implements Serializable { Set abilities = asThoughEffectsMap.get(type).getAbility(effect.getId()); for (Ability ability : abilities) { if (affectedAbility == null) { - // applies to own ability (one effect can be used in multiple abilities) + // applies to full object (one effect can be used in multiple abilities) if (effect.applies(idToCheck, ability, controllerId, game)) { if (effect.isConsumable() && !game.inCheckPlayableState()) { possibleApprovingObjects.add(new ApprovingObject(ability, game)); @@ -559,7 +564,7 @@ public class ContinuousEffects implements Serializable { } } } else { - // applies to affected ability + // applies to one affected ability // filter play abilities (no need to check it in every effect's code) if (type.needPlayCardAbility() && !affectedAbility.getAbilityType().isPlayCardAbility()) { @@ -576,6 +581,7 @@ public class ContinuousEffects implements Serializable { } } } + if (possibleApprovingObjects.size() == 1) { return possibleApprovingObjects.iterator().next(); } else if (possibleApprovingObjects.size() > 1) { @@ -601,7 +607,6 @@ public class ContinuousEffects implements Serializable { } return null; - } public ManaType asThoughMana(ManaType manaType, ManaPoolItem mana, UUID objectId, Ability affectedAbility, UUID controllerId, Game game) { @@ -1390,18 +1395,19 @@ public class ContinuousEffects implements Serializable { } return controllerFound; } - - /** + + /** * Prints out a status of the currently existing continuous effects + * * @param game */ public void traceContinuousEffects(Game game) { game.getContinuousEffects().getLayeredEffects(game); logger.info("-------------------------------------------------------------------------------------------------"); int numberEffects = 0; - for(ContinuousEffectsList list: allEffectsLists) { - numberEffects += list.size(); - } + for (ContinuousEffectsList list : allEffectsLists) { + numberEffects += list.size(); + } logger.info("Turn: " + game.getTurnNum() + " - currently existing continuous effects: " + numberEffects); logger.info("layeredEffects ...................: " + layeredEffects.size()); logger.info("continuousRuleModifyingEffects ...: " + continuousRuleModifyingEffects.size()); @@ -1415,10 +1421,10 @@ public class ContinuousEffects implements Serializable { logger.info("asThoughEffects:"); for (Map.Entry> entry : asThoughEffectsMap.entrySet()) { logger.info("... " + entry.getKey().toString() + ": " + entry.getValue().size()); - } - logger.info("applyCounters ....................: " + (applyCounters != null ? "exists":"null")); - logger.info("auraReplacementEffect ............: " + (continuousRuleModifyingEffects != null ? "exists":"null")); - Map orderedEffects = new TreeMap<>(); + } + logger.info("applyCounters ....................: " + (applyCounters != null ? "exists" : "null")); + logger.info("auraReplacementEffect ............: " + (continuousRuleModifyingEffects != null ? "exists" : "null")); + Map orderedEffects = new TreeMap<>(); traceAddContinuousEffects(orderedEffects, layeredEffects, game, "layeredEffects................"); traceAddContinuousEffects(orderedEffects, continuousRuleModifyingEffects, game, "continuousRuleModifyingEffects"); traceAddContinuousEffects(orderedEffects, replacementEffects, game, "replacementEffects............"); @@ -1430,28 +1436,29 @@ public class ContinuousEffects implements Serializable { traceAddContinuousEffects(orderedEffects, spliceCardEffects, game, "spliceCardEffects............."); for (Map.Entry> entry : asThoughEffectsMap.entrySet()) { traceAddContinuousEffects(orderedEffects, entry.getValue(), game, entry.getKey().toString()); - } + } String playerName = ""; - for (Map.Entry entry : orderedEffects.entrySet()) { + for (Map.Entry entry : orderedEffects.entrySet()) { if (!entry.getValue().getPlayerName().equals(playerName)) { playerName = entry.getValue().getPlayerName(); logger.info("--- Player: " + playerName + " --------------------------------"); - } - logger.info(entry.getValue().getInfo() - + " " + entry.getValue().getSourceName() + } + logger.info(entry.getValue().getInfo() + + " " + entry.getValue().getSourceName() + " " + entry.getValue().getDuration().name() + " " + entry.getValue().getRule() - + " (Order: "+entry.getValue().getOrder() +")" + + " (Order: " + entry.getValue().getOrder() + ")" ); - } + } logger.info("---- End trace Continuous effects --------------------------------------------------------------------------"); } + public static void traceAddContinuousEffects(Map orderedEffects, ContinuousEffectsList cel, Game game, String listName) { for (ContinuousEffect effect : cel) { Set abilities = cel.getAbility(effect.getId()); for (Ability ability : abilities) { Player controller = game.getPlayer(ability.getControllerId()); - MageObject source = game.getObject(ability.getSourceId()); + MageObject source = game.getObject(ability.getSourceId()); TraceInfo traceInfo = new TraceInfo(); traceInfo.setInfo(listName); traceInfo.setOrder(effect.getOrder()); @@ -1459,16 +1466,16 @@ public class ContinuousEffects implements Serializable { traceInfo.setPlayerName("Mage Singleton"); traceInfo.setSourceName("Mage Singleton"); } else { - traceInfo.setPlayerName(controller == null ? "no controller": controller.getName()); - traceInfo.setSourceName(source == null ? "no source": source.getIdName()); - } + traceInfo.setPlayerName(controller == null ? "no controller" : controller.getName()); + traceInfo.setSourceName(source == null ? "no source" : source.getIdName()); + } traceInfo.setRule(ability.getRule()); traceInfo.setAbilityId(ability.getId()); - traceInfo.setEffectId(effect.getId()); + traceInfo.setEffectId(effect.getId()); traceInfo.setDuration(effect.getDuration()); orderedEffects.put(traceInfo.getPlayerName() + traceInfo.getSourceName() + effect.getId() + ability.getId(), traceInfo); } - } + } } - + } diff --git a/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneTargetEffect.java index 4c438da8c16..1ec1a91e7a7 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneTargetEffect.java @@ -1,26 +1,24 @@ package mage.abilities.effects.common.asthought; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.UUID; import mage.MageObject; import mage.abilities.Ability; +import mage.abilities.PlayLandAbility; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.ContinuousEffect; import mage.cards.Card; -import mage.constants.AsThoughEffectType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.TargetController; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Game; import mage.players.Player; import mage.target.targetpointer.FixedTargets; import mage.util.CardUtil; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + /** - * * @author LevelX2 */ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl { @@ -28,6 +26,7 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl { private final Zone fromZone; private final TargetController allowedCaster; private final boolean withoutMana; + private final boolean onlyCastAllowed; // can cast spells, but can't play lands public PlayFromNotOwnHandZoneTargetEffect() { this(Duration.EndOfTurn); @@ -46,10 +45,15 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl { } public PlayFromNotOwnHandZoneTargetEffect(Zone fromZone, TargetController allowedCaster, Duration duration, boolean withoutMana) { + this(fromZone, allowedCaster, duration, withoutMana, false); + } + + public PlayFromNotOwnHandZoneTargetEffect(Zone fromZone, TargetController allowedCaster, Duration duration, boolean withoutMana, boolean onlyCastAllowed) { super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, duration, withoutMana ? Outcome.PlayForFree : Outcome.PutCardInPlay); this.fromZone = fromZone; this.allowedCaster = allowedCaster; this.withoutMana = withoutMana; + this.onlyCastAllowed = onlyCastAllowed; } public PlayFromNotOwnHandZoneTargetEffect(final PlayFromNotOwnHandZoneTargetEffect effect) { @@ -57,6 +61,7 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl { this.fromZone = effect.fromZone; this.allowedCaster = effect.allowedCaster; this.withoutMana = effect.withoutMana; + this.onlyCastAllowed = effect.onlyCastAllowed; } @Override @@ -76,12 +81,25 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl { @Override public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { + if (affectedAbility == null) { + // ContinuousEffects.asThough already checks affectedAbility, so that error must never be called here + // PLAY_FROM_NOT_OWN_HAND_ZONE must applies to affectedAbility only + throw new IllegalArgumentException("ERROR, can't call applies method on empty affectedAbility"); + } + // invalid targets List targets = getTargetPointer().getTargets(game, source); if (targets.isEmpty()) { this.discard(); return false; } + + // invalid zone + if (!game.getState().getZone(objectId).match(fromZone)) { + return false; + } + + // invalid caster switch (allowedCaster) { case YOU: if (playerId != source.getControllerId()) { @@ -101,41 +119,50 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl { case ANY: break; } + + // targets goes to main card all the time UUID objectIdToCast = CardUtil.getMainCardId(game, objectId); - if (targets.contains(objectIdToCast) - && playerId.equals(source.getControllerId()) - && game.getState().getZone(objectId).match(fromZone)) { - if (withoutMana) { - if (affectedAbility != null) { - objectIdToCast = affectedAbility.getSourceId(); - } - return allowCardToPlayWithoutMana(objectIdToCast, source, playerId, game); - } - return true; + if (!targets.contains(objectIdToCast)) { + return false; } - return false; + + // if can't play lands + if (!affectedAbility.getAbilityType().isPlayCardAbility() + || onlyCastAllowed && affectedAbility instanceof PlayLandAbility) { + return false; + } + + // OK, allow to play + if (withoutMana) { + allowCardToPlayWithoutMana(objectId, source, playerId, game); + } + return true; } - - public static boolean exileAndPlayFromExile(Game game, Ability source, Card card, TargetController allowedCaster, Duration duration, boolean withoutMana) { + + public static boolean exileAndPlayFromExile(Game game, Ability source, Card card, TargetController allowedCaster, + Duration duration, boolean withoutMana, boolean onlyCastAllowed) { if (card == null) { return true; } Set cards = new HashSet<>(); cards.add(card); - return exileAndPlayFromExile(game, source, cards, allowedCaster, duration, withoutMana); - } + return exileAndPlayFromExile(game, source, cards, allowedCaster, duration, withoutMana, onlyCastAllowed); + } + /** * Exiles the cards and let the allowed player play them from exile for the given duration - * + * * @param game * @param source * @param cards * @param allowedCaster * @param duration * @param withoutMana - * @return + * @param onlyCastAllowed true for rule "cast that card" and false for rule "play that card" + * @return */ - public static boolean exileAndPlayFromExile(Game game, Ability source, Set cards, TargetController allowedCaster, Duration duration, boolean withoutMana) { + public static boolean exileAndPlayFromExile(Game game, Ability source, Set cards, TargetController allowedCaster, + Duration duration, boolean withoutMana, boolean onlyCastAllowed) { if (cards == null || cards.isEmpty()) { return true; } @@ -149,17 +176,25 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl { + "-" + game.getState().getTurnNum() + "-" + sourceObject.getIdName(), game ); - String exileName = sourceObject.getIdName() + " free play" - + (Duration.EndOfTurn.equals(duration) ? " on turn " + game.getState().getTurnNum():"") + String exileName = sourceObject.getIdName() + " free play" + + (Duration.EndOfTurn.equals(duration) ? " on turn " + game.getState().getTurnNum() : "") + " for " + controller.getName(); if (Duration.EndOfTurn.equals(duration)) { game.getExile().createZone(exileId, exileName).setCleanupOnEndTurn(true); - } + } if (!controller.moveCardsToExile(cards, source, game, true, exileId, exileName)) { return false; } - ContinuousEffect effect = new PlayFromNotOwnHandZoneTargetEffect(Zone.EXILED, allowedCaster, duration, withoutMana); - effect.setTargetPointer(new FixedTargets(cards, game)); + + // get real cards (if it was called on permanent instead card, example: Release to the Wind) + Set cardsToPlay = cards + .stream() + .map(Card::getMainCard) + .filter(card -> Zone.EXILED.equals(game.getState().getZone(card.getId()))) + .collect(Collectors.toSet()); + + ContinuousEffect effect = new PlayFromNotOwnHandZoneTargetEffect(Zone.EXILED, allowedCaster, duration, withoutMana, onlyCastAllowed); + effect.setTargetPointer(new FixedTargets(cardsToPlay, game)); game.addEffect(effect, source); return true; } diff --git a/Mage/src/main/java/mage/abilities/icon/abilities/TrampleAbilityIcon.java b/Mage/src/main/java/mage/abilities/icon/abilities/TrampleAbilityIcon.java index 2b60b10786a..c8811d1d766 100644 --- a/Mage/src/main/java/mage/abilities/icon/abilities/TrampleAbilityIcon.java +++ b/Mage/src/main/java/mage/abilities/icon/abilities/TrampleAbilityIcon.java @@ -21,7 +21,7 @@ public enum TrampleAbilityIcon implements CardIcon { @Override public String getHint() { - return "Trumple ability"; + return "Trample ability"; } @Override diff --git a/Mage/src/main/java/mage/abilities/keyword/FlyingAbility.java b/Mage/src/main/java/mage/abilities/keyword/FlyingAbility.java index dc682908efa..9f1b1557b5a 100644 --- a/Mage/src/main/java/mage/abilities/keyword/FlyingAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/FlyingAbility.java @@ -68,7 +68,7 @@ class FlyingEffect extends RestrictionEffect implements MageSingleton { public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) { return blocker.getAbilities().containsKey(FlyingAbility.getInstance().getId()) || blocker.getAbilities().containsKey(ReachAbility.getInstance().getId()) - || (null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_DRAGON, source, blocker.getControllerId(), game) + || (null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_DRAGON, null, blocker.getControllerId(), game) && attacker.hasSubtype(SubType.DRAGON, game)); } diff --git a/Mage/src/main/java/mage/abilities/keyword/LandwalkAbility.java b/Mage/src/main/java/mage/abilities/keyword/LandwalkAbility.java index fad955ee02c..b69bfa2b356 100644 --- a/Mage/src/main/java/mage/abilities/keyword/LandwalkAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/LandwalkAbility.java @@ -65,18 +65,18 @@ class LandwalkEffect extends RestrictionEffect { @Override public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) { if (game.getBattlefield().contains(filter, source.getSourceId(), blocker.getControllerId(), game, 1) - && null == game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_LANDWALK, source, blocker.getControllerId(), game)) { + && null == game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_LANDWALK, null, blocker.getControllerId(), game)) { switch (filter.getMessage()) { case "plains": - return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_PLAINSWALK, source, blocker.getControllerId(), game); + return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_PLAINSWALK, null, blocker.getControllerId(), game); case "island": - return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_ISLANDWALK, source, blocker.getControllerId(), game); + return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_ISLANDWALK, null, blocker.getControllerId(), game); case "swamp": - return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SWAMPWALK, source, blocker.getControllerId(), game); + return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SWAMPWALK, null, blocker.getControllerId(), game); case "mountain": - return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_MOUNTAINWALK, source, blocker.getControllerId(), game); + return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_MOUNTAINWALK, null, blocker.getControllerId(), game); case "forest": - return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_FORESTWALK, source, blocker.getControllerId(), game); + return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_FORESTWALK, null, blocker.getControllerId(), game); default: return false; } diff --git a/Mage/src/main/java/mage/abilities/keyword/ShadowAbility.java b/Mage/src/main/java/mage/abilities/keyword/ShadowAbility.java index ed1689c4e1c..2aa9a801606 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ShadowAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ShadowAbility.java @@ -70,7 +70,7 @@ class ShadowEffect extends RestrictionEffect implements MageSingleton { @Override public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) { return blocker.getAbilities().containsKey(ShadowAbility.getInstance().getId()) - || null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SHADOW, source, blocker.getControllerId(), game); + || null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SHADOW, null, blocker.getControllerId(), game); } @Override diff --git a/Mage/src/main/java/mage/constants/AsThoughEffectType.java b/Mage/src/main/java/mage/constants/AsThoughEffectType.java index 6bd79a56580..10838cdb936 100644 --- a/Mage/src/main/java/mage/constants/AsThoughEffectType.java +++ b/Mage/src/main/java/mage/constants/AsThoughEffectType.java @@ -1,12 +1,22 @@ package mage.constants; /** + * Allows to change game rules and logic by special conditionals from ContinuousEffects (example: play card from non hand) + *

+ * Can be applied to the game, object or specific ability (affectedAbility param in ContinuousEffects.asThough) + *

+ * There are two usage styles possible: + * - object specific rules and conditionals (use ContinuousEffects.asThough) + * - global rules (use game.getState().getContinuousEffects().getApplicableAsThoughEffects) + * Each AsThough type supports only one usage style + * * @author North */ public enum AsThoughEffectType { ATTACK, ATTACK_AS_HASTE, - ACTIVATE_HASTE, + ACTIVATE_HASTE(true, false), + // BLOCK_TAPPED, BLOCK_SHADOW, BLOCK_DRAGON, @@ -16,48 +26,55 @@ public enum AsThoughEffectType { BLOCK_SWAMPWALK, BLOCK_MOUNTAINWALK, BLOCK_FORESTWALK, + // DAMAGE_NOT_BLOCKED, - BE_BLOCKED, - + // // PLAY_FROM_NOT_OWN_HAND_ZONE + CAST_AS_INSTANT: // 1. Do not use dialogs in "applies" method for that type of effect (it calls multiple times and will freeze the game) - // 2. All effects in "applies" must checks affectedControllerId.equals(source.getControllerId()) (if not then all players will be able to play it) - // 3. Target points to mainCard, but card's characteristics from objectId (split, adventure) + // 2. All effects in "applies" must checks affectedControllerId/playerId.equals(source.getControllerId()) (if not then all players will be able to play it) + // 3. Target must points to mainCard, but checking goes for every card's parts and characteristics from objectId (split, adventure) + // 4. You must implement/override an applies method with "Ability affectedAbility" (e.g. check multiple play/cast abilities from all card's parts) // TODO: search all PLAY_FROM_NOT_OWN_HAND_ZONE and CAST_AS_INSTANT effects and add support of mainCard and objectId - PLAY_FROM_NOT_OWN_HAND_ZONE(true), - CAST_AS_INSTANT(true), - - ACTIVATE_AS_INSTANT, - DAMAGE, + PLAY_FROM_NOT_OWN_HAND_ZONE(true, true), + CAST_AS_INSTANT(true, true), + // + ACTIVATE_AS_INSTANT(true, false), + // SHROUD, HEXPROOF, - PAY_0_ECHO, + // + PAY_0_ECHO(true, false), LOOK_AT_FACE_DOWN, - + // // SPEND_OTHER_MANA: // 1. It's uses for mana calcs at any zone, not stack only // 2. Compare zone change counter as "objectZCC <= targetZCC + 1" // 3. Compare zone with original (like exiled) and stack, not stack only // TODO: search all SPEND_ONLY_MANA effects and improve counters compare as SPEND_OTHER_MANA - SPEND_OTHER_MANA, - + SPEND_OTHER_MANA(true, false), + // SPEND_ONLY_MANA, - TARGET, - + // // ALLOW_FORETELL_ANYTIME: // For Cosmos Charger effect ALLOW_FORETELL_ANYTIME; - private final boolean needPlayCardAbility; // mark effect type as compatible with play/cast abilities + private final boolean needAffectedAbility; // mark what AsThough check must be called for specific ability, not full object (example: spell check) + private final boolean needPlayCardAbility; // mark what AsThough check must be called for play/cast abilities AsThoughEffectType() { - this(false); + this(false, false); } - AsThoughEffectType(boolean needPlayCardAbility) { + AsThoughEffectType(boolean needAffectedAbility, boolean needPlayCardAbility) { + this.needAffectedAbility = needAffectedAbility; this.needPlayCardAbility = needPlayCardAbility; } + public boolean needAffectedAbility() { + return needAffectedAbility; + } + public boolean needPlayCardAbility() { return needPlayCardAbility; } diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java index 3d5d81ce473..4831dc23158 100644 --- a/Mage/src/main/java/mage/game/combat/CombatGroup.java +++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java @@ -169,8 +169,8 @@ public class CombatGroup implements Serializable, Copyable { if ((attacker.getAbilities().containsKey(DamageAsThoughNotBlockedAbility.getInstance().getId()) && player.chooseUse(Outcome.Damage, "Do you wish to assign damage for " + attacker.getLogName() + " as though it weren't blocked?", null, game)) || - game.getContinuousEffects().asThough(attacker.getId(), AsThoughEffectType.DAMAGE_NOT_BLOCKED - , null, attacker.getControllerId(), game) != null) { + game.getContinuousEffects().asThough(attacker.getId(), AsThoughEffectType.DAMAGE_NOT_BLOCKED, + null, attacker.getControllerId(), game) != null) { // for handling creatures like Thorn Elemental blocked = false; unblockedDamage(first, game); @@ -672,16 +672,12 @@ public class CombatGroup implements Serializable, Copyable { if (attackers.contains(creatureId)) { attackers.remove(creatureId); result = true; - if (attackerOrder.contains(creatureId)) { - attackerOrder.remove(creatureId); - } + attackerOrder.remove(creatureId); } else if (blockers.contains(creatureId)) { blockers.remove(creatureId); result = true; //20100423 - 509.2a - if (blockerOrder.contains(creatureId)) { - blockerOrder.remove(creatureId); - } + blockerOrder.remove(creatureId); } return result; } @@ -854,10 +850,8 @@ public class CombatGroup implements Serializable, Copyable { } } } - if (appliesBandsWithOther(attackers, game)) { // 702.21k - both a [quality] creature with “bands with other [quality]” and another [quality] creature (...) - return true; - } - return false; + // 702.21k - both a [quality] creature with “bands with other [quality]” and another [quality] creature (...) + return appliesBandsWithOther(attackers, game); } /** diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 9ae4ef3e07f..6599bd815ba 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -4011,7 +4011,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) { if (null != game.getContinuousEffects().asThough(card.getId(), - AsThoughEffectType.LOOK_AT_FACE_DOWN, card.getSpellAbility(), this.getId(), game)) { + AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game)) { // two modes: look at the card or do not look and activate other abilities String lookMessage = "Look at " + card.getIdName(); String lookYes = "Yes, look at the card";