diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index 6d2a2ae3c78..3bdfa49825a 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -1513,7 +1513,9 @@ public class ComputerPlayer extends PlayerImpl implements Player { protected boolean playManaHandling(Ability ability, ManaCost unpaid, final Game game) { // log.info("paying for " + unpaid.getText()); - ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(), AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); + Set approvingObjects = game.getContinuousEffects().asThough(ability.getSourceId(), AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); + boolean hasApprovingObject = !approvingObjects.isEmpty(); + ManaCost cost; List producers; if (unpaid instanceof ManaCosts) { @@ -1543,7 +1545,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } - if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { + if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { @@ -1560,11 +1562,11 @@ public class ComputerPlayer extends PlayerImpl implements Player { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof ColoredManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { - if (cost.testPay(netMana) || approvingObject != null) { + if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } - if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { + if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { @@ -1578,11 +1580,11 @@ public class ComputerPlayer extends PlayerImpl implements Player { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof SnowManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { - if (cost.testPay(netMana) || approvingObject != null) { + if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } - if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { + if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { @@ -1596,11 +1598,11 @@ public class ComputerPlayer extends PlayerImpl implements Player { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof HybridManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { - if (cost.testPay(netMana) || approvingObject != null) { + if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } - if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { + if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { @@ -1614,11 +1616,11 @@ public class ComputerPlayer extends PlayerImpl implements Player { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof MonoHybridManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { - if (cost.testPay(netMana) || approvingObject != null) { + if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } - if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { + if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { @@ -1632,11 +1634,11 @@ public class ComputerPlayer extends PlayerImpl implements Player { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof ColorlessManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { - if (cost.testPay(netMana) || approvingObject != null) { + if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } - if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { + if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { @@ -1650,11 +1652,11 @@ public class ComputerPlayer extends PlayerImpl implements Player { for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof GenericManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { - if (cost.testPay(netMana) || approvingObject != null) { + if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } - if (approvingObject != null && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { + if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { @@ -1673,7 +1675,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { // pay phyrexian life costs if (cost.isPhyrexian()) { alreadyTryingToPayPhyrexian = true; - boolean paidPhyrexian = cost.pay(ability, game, ability, playerId, false, null) || approvingObject != null; + boolean paidPhyrexian = cost.pay(ability, game, ability, playerId, false, null) || hasApprovingObject; alreadyTryingToPayPhyrexian = false; return paidPhyrexian; } @@ -1688,7 +1690,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { ManaOptions specialMana = specialAction == null ? null : specialAction.getManaOptions(ability, game, unpaid); if (specialMana != null) { for (Mana netMana : specialMana) { - if (cost.testPay(netMana) || approvingObject != null) { + if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 2a5c90aaeae..bed27151fc2 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -1,5 +1,6 @@ package mage.player.human; +import mage.MageIdentifier; import mage.MageObject; import mage.abilities.*; import mage.abilities.costs.VariableCost; @@ -16,6 +17,8 @@ import mage.cards.decks.Deck; import mage.choices.Choice; import mage.choices.ChoiceImpl; import mage.constants.*; +import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL; +import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL; import mage.filter.StaticFilters; import mage.filter.common.FilterAttackingCreature; import mage.filter.common.FilterBlockingCreature; @@ -51,9 +54,6 @@ import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.stream.Collectors; -import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL; -import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL; - /** * @author BetaSteward_at_googlemail.com */ @@ -2260,7 +2260,7 @@ public class HumanPlayer extends PlayerImpl { } // hide on alternative cost activated - if (!getCastSourceIdWithAlternateMana().contains(ability.getSourceId()) + if (!getCastSourceIdWithAlternateMana().getOrDefault(ability.getSourceId(), Collections.emptySet()).contains(MageIdentifier.Default) && ability.getManaCostsToPay().manaValue() > 0) { return true; } diff --git a/Mage.Sets/src/mage/cards/a/AminatousAugury.java b/Mage.Sets/src/mage/cards/a/AminatousAugury.java index 8d59ec9373d..82570d045b0 100644 --- a/Mage.Sets/src/mage/cards/a/AminatousAugury.java +++ b/Mage.Sets/src/mage/cards/a/AminatousAugury.java @@ -1,26 +1,13 @@ package mage.cards.a; -import java.util.EnumSet; -import java.util.Set; -import java.util.UUID; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.OneShotEffect; -import mage.cards.Card; -import mage.cards.CardImpl; -import mage.cards.CardSetInfo; -import mage.cards.Cards; -import mage.cards.CardsImpl; -import mage.cards.ModalDoubleFacedCard; -import mage.cards.ModalDoubleFacedCardHalf; +import mage.cards.*; import mage.choices.Choice; import mage.choices.ChoiceImpl; -import mage.constants.AsThoughEffectType; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.StaticFilters; import mage.game.ExileZone; import mage.game.Game; @@ -29,6 +16,10 @@ import mage.target.TargetCard; import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; +import java.util.EnumSet; +import java.util.Set; +import java.util.UUID; + /** * * @author credman0 @@ -218,7 +209,7 @@ class AminatousAuguryCastFromExileEffect extends AsThoughEffectImpl { usedCardTypes.addAll(unusedCardTypes); game.getState().setValue(source.getSourceId().toString() + "cardTypes", usedCardTypes); } - player.setCastSourceIdWithAlternateMana(objectId, null, card.getSpellAbility().getCosts()); + allowCardToPlayWithoutMana(objectId, source, player.getId(), game); return true; } } diff --git a/Mage.Sets/src/mage/cards/b/BolassCitadel.java b/Mage.Sets/src/mage/cards/b/BolassCitadel.java index 152836f720f..eff8385f182 100644 --- a/Mage.Sets/src/mage/cards/b/BolassCitadel.java +++ b/Mage.Sets/src/mage/cards/b/BolassCitadel.java @@ -1,5 +1,6 @@ package mage.cards.b; +import mage.MageIdentifier; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleStaticAbility; @@ -44,7 +45,7 @@ public final class BolassCitadel extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may play the top card of your library. If you cast a spell this way, pay life equal to its converted mana cost rather than pay its mana cost. - this.addAbility(new SimpleStaticAbility(new BolassCitadelPlayTheTopCardEffect())); + this.addAbility(new SimpleStaticAbility(new BolassCitadelPlayTheTopCardEffect()).setIdentifier(MageIdentifier.BolassCitadelAlternateCast)); // {T}, Sacrifice ten nonland permanents: Each opponent loses 10 life. Ability ability = new SimpleActivatedAbility(new LoseLifeOpponentsEffect(10), new TapSourceCost()); @@ -118,7 +119,7 @@ class BolassCitadelPlayTheTopCardEffect extends AsThoughEffectImpl { Costs newCosts = new CostsImpl(); newCosts.add(lifeCost); newCosts.addAll(cardToCheck.getSpellAbility().getCosts()); - player.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts); + player.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts, MageIdentifier.BolassCitadelAlternateCast); } return true; } diff --git a/Mage.Sets/src/mage/cards/c/CemeteryIlluminator.java b/Mage.Sets/src/mage/cards/c/CemeteryIlluminator.java index 6e060f34d82..3caf95d25dc 100644 --- a/Mage.Sets/src/mage/cards/c/CemeteryIlluminator.java +++ b/Mage.Sets/src/mage/cards/c/CemeteryIlluminator.java @@ -104,7 +104,7 @@ class CemeteryIlluminatorExileEffect extends OneShotEffect { class CemeteryIlluminatorPlayTopEffect extends AsThoughEffectImpl { public CemeteryIlluminatorPlayTopEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); staticText = "Once each turn, you may cast a spell from the top of your library if it shares a card type with a card exiled with {this}"; } diff --git a/Mage.Sets/src/mage/cards/d/DanithaNewBenaliasLight.java b/Mage.Sets/src/mage/cards/d/DanithaNewBenaliasLight.java index 7794b0e27eb..c41cc0dffd6 100644 --- a/Mage.Sets/src/mage/cards/d/DanithaNewBenaliasLight.java +++ b/Mage.Sets/src/mage/cards/d/DanithaNewBenaliasLight.java @@ -63,7 +63,7 @@ public final class DanithaNewBenaliasLight extends CardImpl { class DanithaNewBenaliasLightCastFromGraveyardEffect extends AsThoughEffectImpl { DanithaNewBenaliasLightCastFromGraveyardEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCardInPlay, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCardInPlay); staticText = "once during each of your turns, you may cast an Aura or Equipment spell from your graveyard"; } diff --git a/Mage.Sets/src/mage/cards/d/Demilich.java b/Mage.Sets/src/mage/cards/d/Demilich.java index 718a60566b0..5fbf2090f7d 100644 --- a/Mage.Sets/src/mage/cards/d/Demilich.java +++ b/Mage.Sets/src/mage/cards/d/Demilich.java @@ -1,7 +1,6 @@ package mage.cards.d; -import java.util.UUID; - +import mage.MageIdentifier; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; @@ -17,9 +16,9 @@ import mage.abilities.effects.Effect; import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; import mage.abilities.hint.ValueHint; -import mage.constants.*; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.*; import mage.filter.StaticFilters; import mage.game.Game; import mage.game.stack.Spell; @@ -27,6 +26,8 @@ import mage.players.Player; import mage.target.common.TargetCardInYourGraveyard; import mage.watchers.common.SpellsCastWatcher; +import java.util.UUID; + /** * * @author weirddan455 @@ -53,7 +54,7 @@ public final class Demilich extends CardImpl { this.addAbility(ability); // You may cast Demilich from your graveyard by exiling four instants and/or sorcery cards from your graveyard in addition to paying its other costs. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemilichPlayEffect())); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemilichPlayEffect()).setIdentifier(MageIdentifier.DemilichAlternateCast)); } private Demilich(final Demilich card) { @@ -123,7 +124,7 @@ class DemilichPlayEffect extends AsThoughEffectImpl { if (controller != null) { Costs costs = new CostsImpl<>(); costs.add(new ExileFromGraveCost(new TargetCardInYourGraveyard(4, StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD))); - controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{U}{U}{U}{U}"), costs); + controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{U}{U}{U}{U}"), costs, MageIdentifier.DemilichAlternateCast); return true; } } diff --git a/Mage.Sets/src/mage/cards/d/DemonicEmbrace.java b/Mage.Sets/src/mage/cards/d/DemonicEmbrace.java index 33c970cb1b7..74d099e4956 100644 --- a/Mage.Sets/src/mage/cards/d/DemonicEmbrace.java +++ b/Mage.Sets/src/mage/cards/d/DemonicEmbrace.java @@ -1,7 +1,8 @@ package mage.cards.d; -import java.util.UUID; - +import mage.MageIdentifier; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.Costs; import mage.abilities.costs.CostsImpl; @@ -9,22 +10,22 @@ import mage.abilities.costs.common.DiscardCardCost; import mage.abilities.costs.common.PayLifeCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.AsThoughEffectImpl; -import mage.constants.*; -import mage.game.Game; -import mage.players.Player; -import mage.target.common.TargetCreaturePermanent; -import mage.abilities.Ability; -import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.Effect; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.continuous.AddCardSubtypeAttachedEffect; import mage.abilities.effects.common.continuous.BoostEnchantedEffect; import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; -import mage.target.TargetPermanent; import mage.abilities.keyword.EnchantAbility; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; /** * @author arcox @@ -54,7 +55,7 @@ public final class DemonicEmbrace extends CardImpl { this.addAbility(ability); // You may cast Demonic Embrace from your graveyard by paying 3 life and discarding a card in addition to paying its other costs. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemonicEmbracePlayEffect())); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new DemonicEmbracePlayEffect()).setIdentifier(MageIdentifier.DemonicEmbraceAlternateCast)); } private DemonicEmbrace(final DemonicEmbrace card) { @@ -98,7 +99,10 @@ class DemonicEmbracePlayEffect extends AsThoughEffectImpl { Costs costs = new CostsImpl<>(); costs.add(new PayLifeCost(3)); costs.add(new DiscardCardCost()); - player.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{1}{B}{B}"), costs); + player.setCastSourceIdWithAlternateMana( + sourceId, new ManaCostsImpl<>("{1}{B}{B}"), costs, + MageIdentifier.DemonicEmbraceAlternateCast + ); return true; } } diff --git a/Mage.Sets/src/mage/cards/f/FalcoSparaPactweaver.java b/Mage.Sets/src/mage/cards/f/FalcoSparaPactweaver.java index eca552200ef..d85c8983756 100644 --- a/Mage.Sets/src/mage/cards/f/FalcoSparaPactweaver.java +++ b/Mage.Sets/src/mage/cards/f/FalcoSparaPactweaver.java @@ -1,5 +1,6 @@ package mage.cards.f; +import mage.MageIdentifier; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldAbility; @@ -18,12 +19,12 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.counters.CounterType; +import mage.filter.common.FilterControlledCreaturePermanent; import mage.game.Game; import mage.players.Player; import mage.target.common.TargetControlledCreaturePermanent; import java.util.UUID; -import mage.filter.common.FilterControlledCreaturePermanent; /** * @author TheElk801 @@ -56,7 +57,10 @@ public final class FalcoSparaPactweaver extends CardImpl { this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); // You may cast spells from the top of your library by removing a counter from a creature you control in addition to paying their other costs. - this.addAbility(new SimpleStaticAbility(new FalcoSparaPactweaverEffect())); + this.addAbility( + new SimpleStaticAbility(new FalcoSparaPactweaverEffect()) + .setIdentifier(MageIdentifier.FalcoSparaPactweaverAlternateCast) + ); } private FalcoSparaPactweaver(final FalcoSparaPactweaver card) { @@ -113,7 +117,10 @@ class FalcoSparaPactweaverEffect extends AsThoughEffectImpl { Costs newCosts = new CostsImpl<>(); newCosts.add(new RemoveCounterCost(new TargetControlledCreaturePermanent(1, 1, new FilterControlledCreaturePermanent(), true))); newCosts.addAll(cardToCheck.getSpellAbility().getCosts()); - player.setCastSourceIdWithAlternateMana(cardToCheck.getId(), cardToCheck.getManaCost(), newCosts); + player.setCastSourceIdWithAlternateMana( + cardToCheck.getId(), cardToCheck.getManaCost(), newCosts, + MageIdentifier.FalcoSparaPactweaverAlternateCast + ); return true; } } diff --git a/Mage.Sets/src/mage/cards/g/GisaAndGeralf.java b/Mage.Sets/src/mage/cards/g/GisaAndGeralf.java index 8f0630d5dff..07bd25c2985 100644 --- a/Mage.Sets/src/mage/cards/g/GisaAndGeralf.java +++ b/Mage.Sets/src/mage/cards/g/GisaAndGeralf.java @@ -57,7 +57,7 @@ public final class GisaAndGeralf extends CardImpl { class GisaAndGeralfCastFromGraveyardEffect extends AsThoughEffectImpl { GisaAndGeralfCastFromGraveyardEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCreatureInPlay, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCreatureInPlay); staticText = "During each of your turns, you may cast a Zombie creature spell from your graveyard"; } diff --git a/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java b/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java index d09448fd52f..2bd2814ce19 100644 --- a/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java +++ b/Mage.Sets/src/mage/cards/g/GlimpseTheCosmos.java @@ -1,9 +1,12 @@ package mage.cards.g; -import java.util.HashSet; -import java.util.Set; +import mage.MageIdentifier; import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.decorator.ConditionalAsThoughEffect; +import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; import mage.cards.Card; @@ -15,14 +18,12 @@ import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.ZoneChangeEvent; import mage.players.Player; -import java.util.UUID; -import mage.MageIdentifier; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.decorator.ConditionalAsThoughEffect; -import mage.abilities.effects.AsThoughEffectImpl; import mage.watchers.Watcher; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + /** * * @author jeffwadsworth @@ -36,11 +37,16 @@ public class GlimpseTheCosmos extends CardImpl { this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect(3, 1, PutCards.HAND, PutCards.BOTTOM_ANY)); //As long as you control a Giant, you may cast Glimpse the Cosmos from your graveyard by paying {U} rather than paying its mana cost. If you cast Glimpse the Cosmos this way and it would be put into your graveyard, exile it instead. - this.addAbility(new SimpleStaticAbility(Zone.GRAVEYARD, - new ConditionalAsThoughEffect( - new GlimpseTheCosmosPlayEffect(), - new PermanentsOnTheBattlefieldCondition(new FilterControlledPermanent(SubType.GIANT)))).setIdentifier(MageIdentifier.GlimpseTheCosmosWatcher), - new GlimpseTheCosmosWatcher()); + this.addAbility( + new SimpleStaticAbility( + Zone.GRAVEYARD, + new ConditionalAsThoughEffect( + new GlimpseTheCosmosPlayEffect(), + new PermanentsOnTheBattlefieldCondition(new FilterControlledPermanent(SubType.GIANT)) + ) + ).setIdentifier(MageIdentifier.GlimpseTheCosmosWatcher), + new GlimpseTheCosmosWatcher() + ); this.addAbility(new SimpleStaticAbility(Zone.ALL, new GlimpseTheCosmosReplacementEffect())); @@ -85,7 +91,10 @@ class GlimpseTheCosmosPlayEffect extends AsThoughEffectImpl { if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { Player controller = game.getPlayer(affectedControllerId); if (controller != null) { - controller.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{U}"), null); + controller.setCastSourceIdWithAlternateMana( + sourceId, new ManaCostsImpl<>("{U}"), null, + MageIdentifier.GlimpseTheCosmosWatcher + ); return true; } } diff --git a/Mage.Sets/src/mage/cards/h/HaukensInsight.java b/Mage.Sets/src/mage/cards/h/HaukensInsight.java index 03c6cacc2a6..928239728a8 100644 --- a/Mage.Sets/src/mage/cards/h/HaukensInsight.java +++ b/Mage.Sets/src/mage/cards/h/HaukensInsight.java @@ -136,7 +136,7 @@ class HaukensInsightLookEffect extends AsThoughEffectImpl { class HaukensInsightPlayEffect extends AsThoughEffectImpl { public HaukensInsightPlayEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PlayForFree, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PlayForFree); staticText = "Once during each of your turns, you may play a land or cast a spell from among the cards exiled with this permanent without paying its mana cost"; } @@ -164,7 +164,7 @@ class HaukensInsightPlayEffect extends AsThoughEffectImpl { UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), game.getState().getZoneChangeCounter(source.getSourceId())); ExileZone exileZone = game.getExile().getExileZone(exileId); if (exileZone != null && exileZone.contains(CardUtil.getMainCardId(game, objectId))) { - allowCardToPlayWithoutMana(objectId, source, affectedControllerId, game); + allowCardToPlayWithoutMana(objectId, source, affectedControllerId, MageIdentifier.HaukensInsightWatcher, game); return true; } } diff --git a/Mage.Sets/src/mage/cards/h/Helbrute.java b/Mage.Sets/src/mage/cards/h/Helbrute.java index 340a1777c07..8f6d3d5d8c5 100644 --- a/Mage.Sets/src/mage/cards/h/Helbrute.java +++ b/Mage.Sets/src/mage/cards/h/Helbrute.java @@ -1,5 +1,6 @@ package mage.cards.h; +import mage.MageIdentifier; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; @@ -37,7 +38,9 @@ public final class Helbrute extends CardImpl { this.addAbility(HasteAbility.getInstance()); // Sarcophagus — You may cast Helbrute from your graveyard by exiling another creature card from your graveyard in addition to paying its other costs. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new HelbruteEffect()).withFlavorWord("Sarcophagus")); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new HelbruteEffect()) + .withFlavorWord("Sarcophagus") + .setIdentifier(MageIdentifier.HelbruteAlternateCast)); } private Helbrute(final Helbrute card) { @@ -91,7 +94,10 @@ class HelbruteEffect extends AsThoughEffectImpl { } Costs costs = new CostsImpl<>(); costs.add(new ExileFromGraveCost(new TargetCardInYourGraveyard(filter))); - controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{3}{B}{R}"), costs); + controller.setCastSourceIdWithAlternateMana( + objectId, new ManaCostsImpl<>("{3}{B}{R}"), costs, + MageIdentifier.HelbruteAlternateCast + ); return true; } } diff --git a/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java b/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java index ac0a9e31e43..4bf0023733d 100644 --- a/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java +++ b/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java @@ -84,7 +84,7 @@ enum JohannApprenticeSorcererHint implements Hint { class JohannApprenticeSorcererPlayTopEffect extends AsThoughEffectImpl { public JohannApprenticeSorcererPlayTopEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); staticText = "Once each turn, you may cast an instant or sorcery spell from the top of your library"; } diff --git a/Mage.Sets/src/mage/cards/k/KaghaShadowArchdruid.java b/Mage.Sets/src/mage/cards/k/KaghaShadowArchdruid.java index e0cb4d0aee1..7a9f1d8e5bb 100644 --- a/Mage.Sets/src/mage/cards/k/KaghaShadowArchdruid.java +++ b/Mage.Sets/src/mage/cards/k/KaghaShadowArchdruid.java @@ -72,7 +72,7 @@ public final class KaghaShadowArchdruid extends CardImpl { class KaghaShadowArchdruidEffect extends AsThoughEffectImpl { KaghaShadowArchdruidEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); this.staticText = "Once during each of your turns, you may play a land or cast a permanent spell from among cards in your graveyard that were put there from your library this turn."; } diff --git a/Mage.Sets/src/mage/cards/k/KaradorGhostChieftain.java b/Mage.Sets/src/mage/cards/k/KaradorGhostChieftain.java index c4a665473dd..7f7a8fa882d 100644 --- a/Mage.Sets/src/mage/cards/k/KaradorGhostChieftain.java +++ b/Mage.Sets/src/mage/cards/k/KaradorGhostChieftain.java @@ -98,7 +98,7 @@ class KaradorGhostChieftainCostReductionEffect extends CostModificationEffectImp class KaradorGhostChieftainCastFromGraveyardEffect extends AsThoughEffectImpl { KaradorGhostChieftainCastFromGraveyardEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCreatureInPlay, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCreatureInPlay); staticText = "During each of your turns, you may cast a creature spell from your graveyard"; } diff --git a/Mage.Sets/src/mage/cards/k/KessDissidentMage.java b/Mage.Sets/src/mage/cards/k/KessDissidentMage.java index 7534ba6489d..f9a115432ae 100644 --- a/Mage.Sets/src/mage/cards/k/KessDissidentMage.java +++ b/Mage.Sets/src/mage/cards/k/KessDissidentMage.java @@ -60,7 +60,7 @@ public final class KessDissidentMage extends CardImpl { class KessDissidentMageCastFromGraveyardEffect extends AsThoughEffectImpl { KessDissidentMageCastFromGraveyardEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); staticText = "During each of your turns, you may cast an instant or sorcery card from your graveyard"; } diff --git a/Mage.Sets/src/mage/cards/m/MaestrosAscendancy.java b/Mage.Sets/src/mage/cards/m/MaestrosAscendancy.java index 80d9f158113..7458014e2c7 100644 --- a/Mage.Sets/src/mage/cards/m/MaestrosAscendancy.java +++ b/Mage.Sets/src/mage/cards/m/MaestrosAscendancy.java @@ -1,5 +1,6 @@ package mage.cards.m; +import mage.MageIdentifier; import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; @@ -31,7 +32,8 @@ public final class MaestrosAscendancy extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{U}{B}{R}"); // Once during each of your turns, you may cast an instant or sorcery spell from your graveyard by sacrificing a creature in addition to paying its other costs. If a spell cast this way would be put into your graveyard, exile it instead. - Ability ability = new SimpleStaticAbility(new MaestrosAscendancyCastEffect()); + Ability ability = new SimpleStaticAbility(new MaestrosAscendancyCastEffect()) + .setIdentifier(MageIdentifier.MaestrosAscendencyAlternateCast); ability.addEffect(new MaestrosAscendancyExileEffect()); this.addAbility(ability, new MaestrosAscendancyWatcher()); } @@ -87,7 +89,10 @@ class MaestrosAscendancyCastEffect extends AsThoughEffectImpl { Costs newCosts = new CostsImpl<>(); newCosts.addAll(card.getSpellAbility().getCosts()); newCosts.add(new SacrificeTargetCost(StaticFilters.FILTER_CONTROLLED_CREATURE_SHORT_TEXT)); - player.setCastSourceIdWithAlternateMana(card.getId(), card.getManaCost(), newCosts); + player.setCastSourceIdWithAlternateMana( + card.getId(), card.getManaCost(), newCosts, + MageIdentifier.MaestrosAscendencyAlternateCast + ); return true; } } diff --git a/Mage.Sets/src/mage/cards/m/MuldrothaTheGravetide.java b/Mage.Sets/src/mage/cards/m/MuldrothaTheGravetide.java index 4ef61ce5092..0a7fb351fb4 100644 --- a/Mage.Sets/src/mage/cards/m/MuldrothaTheGravetide.java +++ b/Mage.Sets/src/mage/cards/m/MuldrothaTheGravetide.java @@ -66,7 +66,7 @@ public final class MuldrothaTheGravetide extends CardImpl { class MuldrothaTheGravetideCastFromGraveyardEffect extends AsThoughEffectImpl { public MuldrothaTheGravetideCastFromGraveyardEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); staticText = "During each of your turns, you may play a land and cast a permanent spell of each permanent type from your graveyard. " + "(If a card has multiple permanent types, choose one as you play it.)"; } diff --git a/Mage.Sets/src/mage/cards/n/NashiMoonSagesScion.java b/Mage.Sets/src/mage/cards/n/NashiMoonSagesScion.java index 8e6e06956b9..35bf6dd1c67 100644 --- a/Mage.Sets/src/mage/cards/n/NashiMoonSagesScion.java +++ b/Mage.Sets/src/mage/cards/n/NashiMoonSagesScion.java @@ -1,5 +1,6 @@ package mage.cards.n; +import mage.MageIdentifier; import mage.MageInt; import mage.MageObjectReference; import mage.abilities.Ability; @@ -43,7 +44,7 @@ public final class NashiMoonSagesScion extends CardImpl { // Whenever Nashi, Moon Sage's Scion deals combat damage to a player, exile the top card of each player's library. Until end of turn, you may play one of those cards. If you cast a spell this way, pay life equal to its mana value rather than paying its mana cost. this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility( new NashiMoonSagesScionEffect(), false - ), new NashiMoonSagesScionWatcher()); + ).setIdentifier(MageIdentifier.NashiMoonSagesScionAlternateCast), new NashiMoonSagesScionWatcher()); } private NashiMoonSagesScion(final NashiMoonSagesScion card) { @@ -194,7 +195,10 @@ class NashiMoonSagesScionPlayEffect extends CanPlayCardControllerEffect { Costs newCosts = new CostsImpl<>(); newCosts.add(lifeCost); newCosts.addAll(cardToCheck.getSpellAbility().getCosts()); - controller.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts); + controller.setCastSourceIdWithAlternateMana( + cardToCheck.getId(), null, newCosts, + MageIdentifier.NashiMoonSagesScionAlternateCast + ); return true; } } diff --git a/Mage.Sets/src/mage/cards/o/OneWithTheMultiverse.java b/Mage.Sets/src/mage/cards/o/OneWithTheMultiverse.java index 53ae8926797..733146a5e69 100644 --- a/Mage.Sets/src/mage/cards/o/OneWithTheMultiverse.java +++ b/Mage.Sets/src/mage/cards/o/OneWithTheMultiverse.java @@ -4,15 +4,18 @@ import mage.MageIdentifier; import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.hint.common.ConditionPermanentHint; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.Game; import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.CardUtil; import mage.watchers.Watcher; @@ -22,7 +25,7 @@ import java.util.Set; import java.util.UUID; /** - * @author TheElk801 + * @author TheElk801, Susucr */ public final class OneWithTheMultiverse extends CardImpl { @@ -36,8 +39,15 @@ public final class OneWithTheMultiverse extends CardImpl { this.addAbility(new SimpleStaticAbility(new PlayTheTopCardEffect())); // Once during each of your turns, you may cast a spell from your hand or the top of your library without paying its mana cost. - this.addAbility(new SimpleStaticAbility(new OneWithTheMultiverseEffect()) - .setIdentifier(MageIdentifier.OneWithTheMultiverseWatcher), new OneWithTheMultiverseWatcher()); + this.addAbility( + new SimpleStaticAbility(new OneWithTheMultiverseEffect()) + .setIdentifier(MageIdentifier.OneWithTheMultiverseWatcher) + .addHint(new ConditionPermanentHint( + OneWithTheMultiverseCondition.instance, + "Can cast a spell without paying mana cost this turn" + )), + new OneWithTheMultiverseWatcher() + ); } private OneWithTheMultiverse(final OneWithTheMultiverse card) { @@ -50,10 +60,24 @@ public final class OneWithTheMultiverse extends CardImpl { } } +enum OneWithTheMultiverseCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + OneWithTheMultiverseWatcher watcher = game.getState().getWatcher(OneWithTheMultiverseWatcher.class); + Permanent sourceObject = game.getPermanent(source.getSourceId()); + return watcher != null + && sourceObject != null + && game.isActivePlayer(source.getControllerId()) + && !watcher.isAbilityUsed(new MageObjectReference(sourceObject, game)); + } +} + class OneWithTheMultiverseEffect extends AsThoughEffectImpl { public OneWithTheMultiverseEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PlayForFree, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PlayForFree); staticText = "once during each of your turns, you may cast a spell from your hand " + "or the top of your library without paying its mana cost."; } @@ -78,13 +102,14 @@ class OneWithTheMultiverseEffect extends AsThoughEffectImpl { return false; } Player controller = game.getPlayer(source.getControllerId()); + Permanent sourceObject = game.getPermanent(source.getSourceId()); Card card = game.getCard(CardUtil.getMainCardId(game, objectId)); OneWithTheMultiverseWatcher watcher = game.getState().getWatcher(OneWithTheMultiverseWatcher.class); if (controller == null || card == null || watcher == null - || watcher.isAbilityUsed(new MageObjectReference(source)) - || !card.isOwnedBy(source.getControllerId())) { + || sourceObject == null + || watcher.isAbilityUsed(new MageObjectReference(sourceObject, game))) { return false; } Zone zone = game.getState().getZone(card.getId()); @@ -92,7 +117,8 @@ class OneWithTheMultiverseEffect extends AsThoughEffectImpl { (!Zone.LIBRARY.match(zone) || !controller.getLibrary().getFromTop(game).getId().equals(card.getId()))) { return false; } - allowCardToPlayWithoutMana(objectId, source, affectedControllerId, game); + + allowCardToPlayWithoutMana(objectId, source, affectedControllerId, MageIdentifier.OneWithTheMultiverseWatcher, game); return true; } } diff --git a/Mage.Sets/src/mage/cards/r/RaffinesGuidance.java b/Mage.Sets/src/mage/cards/r/RaffinesGuidance.java index 4cdba9e31a3..d7b46c16d4c 100644 --- a/Mage.Sets/src/mage/cards/r/RaffinesGuidance.java +++ b/Mage.Sets/src/mage/cards/r/RaffinesGuidance.java @@ -1,5 +1,6 @@ package mage.cards.r; +import mage.MageIdentifier; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.Cost; @@ -39,8 +40,9 @@ public final class RaffinesGuidance extends CardImpl { // Enchanted creature gets +1/+1. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostEnchantedEffect(1, 1, Duration.WhileOnBattlefield))); - // You may cast Raffine’s Guidance from your graveyard by paying {2}{W} instead of its mana cost. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new RafinnesGuidancePlayEffect())); + // You may cast Raffine's Guidance from your graveyard by paying {2}{W} instead of its mana cost. + this.addAbility(new SimpleStaticAbility(Zone.ALL, new RafinnesGuidancePlayEffect()) + .setIdentifier(MageIdentifier.RafinnesGuidanceAlternateCast)); } private RaffinesGuidance(final RaffinesGuidance card) { @@ -71,7 +73,10 @@ class RafinnesGuidancePlayEffect extends AsThoughEffectImpl { Player player = game.getPlayer(affectedControllerId); if (player != null) { Costs costs = new CostsImpl<>(); - player.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{2}{W}"), costs); + player.setCastSourceIdWithAlternateMana( + sourceId, new ManaCostsImpl<>("{2}{W}"), costs, + MageIdentifier.RafinnesGuidanceAlternateCast + ); return true; } } diff --git a/Mage.Sets/src/mage/cards/r/RisenExecutioner.java b/Mage.Sets/src/mage/cards/r/RisenExecutioner.java index c136a63a4bc..696a3a1a925 100644 --- a/Mage.Sets/src/mage/cards/r/RisenExecutioner.java +++ b/Mage.Sets/src/mage/cards/r/RisenExecutioner.java @@ -2,10 +2,14 @@ package mage.cards.r; import java.util.UUID; + +import mage.MageIdentifier; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.CantBlockAbility; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.mana.ManaCosts; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.common.continuous.BoostControlledEffect; import mage.abilities.effects.common.cost.CostModificationEffectImpl; @@ -23,7 +27,7 @@ import mage.util.CardUtil; /** * - * @author LevelX2 + * @author LevelX2, Susucr */ public final class RisenExecutioner extends CardImpl { @@ -47,9 +51,8 @@ public final class RisenExecutioner extends CardImpl { this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostControlledEffect(1, 1, Duration.WhileOnBattlefield, filter, true))); // You may cast Risen Executioner from your graveyard if you pay {1} more to cast it for each other creature card in your graveyard. - // TODO: cost increase does not happen if Risen Executioner is cast grom graveyard because of other effects - Ability ability = new SimpleStaticAbility(Zone.ALL, new RisenExecutionerCastEffect()); - ability.addEffect(new RisenExecutionerCostIncreasingEffect()); + Ability ability = new SimpleStaticAbility(Zone.ALL, new RisenExecutionerCastEffect()) + .setIdentifier(MageIdentifier.RisenExectutionerAlternateCast); this.addAbility(ability); } @@ -66,6 +69,12 @@ public final class RisenExecutioner extends CardImpl { class RisenExecutionerCastEffect extends AsThoughEffectImpl { + protected static final FilterCreatureCard filter = new FilterCreatureCard(); + + static { + filter.add(AnotherPredicate.instance); + } + RisenExecutionerCastEffect() { super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.Benefit); staticText = "You may cast {this} from your graveyard if you pay {1} more to cast it for each other creature card in your graveyard"; @@ -87,56 +96,23 @@ class RisenExecutionerCastEffect extends AsThoughEffectImpl { @Override public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - if (sourceId.equals(source.getSourceId())) { - Card card = game.getCard(source.getSourceId()); - if (card != null - && card.isOwnedBy(affectedControllerId) - && game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { - return true; - } + if (!sourceId.equals(source.getSourceId())) { + return false; + } + Card card = game.getCard(source.getSourceId()); + if(card == null + || !card.isOwnedBy(affectedControllerId) + || game.getState().getZone(source.getSourceId()) != Zone.GRAVEYARD) { + return false; } - return false; - } -} -class RisenExecutionerCostIncreasingEffect extends CostModificationEffectImpl { - - protected static final FilterCreatureCard filter = new FilterCreatureCard(); - - static { - filter.add(AnotherPredicate.instance); - } - - RisenExecutionerCostIncreasingEffect() { - super(Duration.EndOfGame, Outcome.Benefit, CostModificationType.INCREASE_COST); - staticText = ""; - } - - private RisenExecutionerCostIncreasingEffect(final RisenExecutionerCostIncreasingEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source, Ability abilityToModify) { Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - CardUtil.increaseCost(abilityToModify, controller.getGraveyard().count(filter, source.getControllerId(), source, game)); + if(controller == null) { + return false; } + int costIncrease = controller.getGraveyard().count(filter, source.getControllerId(), source, game); + ManaCosts adjustedCost = CardUtil.adjustCost(card.getSpellAbility().getManaCostsToPay(), -costIncrease); + controller.setCastSourceIdWithAlternateMana(card.getId(), adjustedCost, null, MageIdentifier.RisenExectutionerAlternateCast); return true; } - - @Override - public boolean applies(Ability abilityToModify, Ability source, Game game) { - if (abilityToModify.getSourceId().equals(source.getSourceId())) { - Spell spell = game.getStack().getSpell(abilityToModify.getSourceId()); - return spell != null && spell.getFromZone() == Zone.GRAVEYARD; - } - return false; - } - - @Override - public RisenExecutionerCostIncreasingEffect copy() { - return new RisenExecutionerCostIncreasingEffect(this); - } - -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/r/RonaSheoldredsFaithful.java b/Mage.Sets/src/mage/cards/r/RonaSheoldredsFaithful.java index e244f487589..d8ab19e18ec 100644 --- a/Mage.Sets/src/mage/cards/r/RonaSheoldredsFaithful.java +++ b/Mage.Sets/src/mage/cards/r/RonaSheoldredsFaithful.java @@ -1,5 +1,6 @@ package mage.cards.r; +import mage.MageIdentifier; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; @@ -42,7 +43,8 @@ public final class RonaSheoldredsFaithful extends CardImpl { )); // You may cast Rona, Sheoldred's Faithful from your graveyard by discarding two cards in addition to paying its other costs. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new RonaSheoldredsFaithfulEffect())); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new RonaSheoldredsFaithfulEffect()) + .setIdentifier(MageIdentifier.RonaSheoldredsFaithfulAlternateCast)); } private RonaSheoldredsFaithful(final RonaSheoldredsFaithful card) { @@ -90,7 +92,10 @@ class RonaSheoldredsFaithfulEffect extends AsThoughEffectImpl { } Costs costs = new CostsImpl<>(); costs.add(new DiscardTargetCost(new TargetCardInHand(2, StaticFilters.FILTER_CARD_CARDS))); - controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{1}{U}{B}{B}"), costs); + controller.setCastSourceIdWithAlternateMana( + objectId, new ManaCostsImpl<>("{1}{U}{B}{B}"), costs, + MageIdentifier.RonaSheoldredsFaithfulAlternateCast + ); return true; } } diff --git a/Mage.Sets/src/mage/cards/s/ScourgeOfNelToth.java b/Mage.Sets/src/mage/cards/s/ScourgeOfNelToth.java index 930e047e5fc..8188fdf1bdf 100644 --- a/Mage.Sets/src/mage/cards/s/ScourgeOfNelToth.java +++ b/Mage.Sets/src/mage/cards/s/ScourgeOfNelToth.java @@ -1,7 +1,7 @@ package mage.cards.s; -import java.util.UUID; +import mage.MageIdentifier; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; @@ -14,16 +14,13 @@ import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.AsThoughEffectType; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Game; import mage.players.Player; import mage.target.common.TargetControlledCreaturePermanent; +import java.util.UUID; + /** * * @author LevelX2 @@ -40,7 +37,8 @@ public final class ScourgeOfNelToth extends CardImpl { // Flying this.addAbility(FlyingAbility.getInstance()); // You may cast Scourge of Nel Toth from your graveyard by paying {B}{B} and sacrificing two creatures rather than paying its mana cost. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new ScourgeOfNelTothPlayEffect())); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new ScourgeOfNelTothPlayEffect()) + .setIdentifier(MageIdentifier.ScourgeOfNelTothAlternateCast)); } private ScourgeOfNelToth(final ScourgeOfNelToth card) { @@ -80,10 +78,12 @@ class ScourgeOfNelTothPlayEffect extends AsThoughEffectImpl { if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { Player player = game.getPlayer(affectedControllerId); if (player != null) { - // can sometimes be cast with base mana cost from grave???? Costs costs = new CostsImpl<>(); costs.add(new SacrificeTargetCost(new TargetControlledCreaturePermanent(2))); - player.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{B}{B}"), costs); + player.setCastSourceIdWithAlternateMana( + sourceId, new ManaCostsImpl<>("{B}{B}"), costs, + MageIdentifier.ScourgeOfNelTothAlternateCast + ); return true; } } diff --git a/Mage.Sets/src/mage/cards/s/SqueeDubiousMonarch.java b/Mage.Sets/src/mage/cards/s/SqueeDubiousMonarch.java index 08a7772a532..dc0b3d5c803 100644 --- a/Mage.Sets/src/mage/cards/s/SqueeDubiousMonarch.java +++ b/Mage.Sets/src/mage/cards/s/SqueeDubiousMonarch.java @@ -1,5 +1,6 @@ package mage.cards.s; +import mage.MageIdentifier; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; @@ -47,7 +48,8 @@ public final class SqueeDubiousMonarch extends CardImpl { ))); // You may cast Squee, Dubious Monarch from your graveyard by paying {3}{R} and exiling four other cards from your graveyard rather than paying its mana cost. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SqueeDubiousMonarchEffect())); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SqueeDubiousMonarchEffect()) + .setIdentifier(MageIdentifier.SqueeDubiousMonarchAlternateCast)); } private SqueeDubiousMonarch(final SqueeDubiousMonarch card) { @@ -101,7 +103,10 @@ class SqueeDubiousMonarchEffect extends AsThoughEffectImpl { } Costs costs = new CostsImpl<>(); costs.add(new ExileFromGraveCost(new TargetCardInYourGraveyard(4, filter))); - controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{3}{R}"), costs); + controller.setCastSourceIdWithAlternateMana( + objectId, new ManaCostsImpl<>("{3}{R}"), costs, + MageIdentifier.SqueeDubiousMonarchAlternateCast + ); return true; } } diff --git a/Mage.Sets/src/mage/cards/t/Tromokratis.java b/Mage.Sets/src/mage/cards/t/Tromokratis.java index f0f11996280..16be7aac13c 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, null, blocker.getControllerId(), game)) { + if (permanent.isTapped() && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, blocker.getControllerId(), game).isEmpty()) { return false; } // check blocker restrictions diff --git a/Mage.Sets/src/mage/cards/w/WellOfKnowledge.java b/Mage.Sets/src/mage/cards/w/WellOfKnowledge.java index a371089fbf8..8e659d230d4 100644 --- a/Mage.Sets/src/mage/cards/w/WellOfKnowledge.java +++ b/Mage.Sets/src/mage/cards/w/WellOfKnowledge.java @@ -1,5 +1,6 @@ package mage.cards.w; +import mage.ApprovingObject; import mage.abilities.Ability; import mage.abilities.ActivatedAbilityImpl; import mage.abilities.condition.common.IsStepCondition; @@ -63,7 +64,7 @@ class WellOfKnowledgeConditionalActivatedAbility extends ActivatedAbilityImpl { && getCosts().canPay(this, this, playerId, game) && game.isActivePlayer(playerId)) { this.activatorId = playerId; - return ActivationStatus.getTrue(this, game); + return new ActivationStatus(new ApprovingObject(this, game)); } return ActivationStatus.getFalse(); diff --git a/Mage.Sets/src/mage/cards/w/Wish.java b/Mage.Sets/src/mage/cards/w/Wish.java index 7b0d8ab81cf..639c0d2bdc1 100644 --- a/Mage.Sets/src/mage/cards/w/Wish.java +++ b/Mage.Sets/src/mage/cards/w/Wish.java @@ -77,7 +77,7 @@ class WishEffect extends OneShotEffect { class WishPlayFromSideboardEffect extends AsThoughEffectImpl { public WishPlayFromSideboardEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit); } private WishPlayFromSideboardEffect(final WishPlayFromSideboardEffect effect) { diff --git a/Mage.Sets/src/mage/cards/w/WorldheartPhoenix.java b/Mage.Sets/src/mage/cards/w/WorldheartPhoenix.java index 4f9d6a156bb..f5a2525d015 100644 --- a/Mage.Sets/src/mage/cards/w/WorldheartPhoenix.java +++ b/Mage.Sets/src/mage/cards/w/WorldheartPhoenix.java @@ -1,7 +1,7 @@ package mage.cards.w; -import java.util.UUID; +import mage.MageIdentifier; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.SpellAbility; @@ -13,17 +13,14 @@ import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.AsThoughEffectType; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.*; import mage.counters.CounterType; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.UUID; + /** * * @author LevelX2 @@ -41,7 +38,8 @@ public final class WorldheartPhoenix extends CardImpl { // You may cast Worldheart Phoenix from your graveyard by paying {W}{U}{B}{R}{G} rather than paying its mana cost. // If you do, it enters the battlefield with two +1/+1 counters on it. - Ability ability = new SimpleStaticAbility(Zone.ALL, new WorldheartPhoenixPlayEffect()); + Ability ability = new SimpleStaticAbility(Zone.ALL, new WorldheartPhoenixPlayEffect()) + .setIdentifier(MageIdentifier.WorldheartPhoenixAlternateCast); ability.addEffect(new EntersBattlefieldEffect(new WorldheartPhoenixEntersBattlefieldEffect(), "If you do, it enters the battlefield with two +1/+1 counters on it")); this.addAbility(ability); @@ -84,8 +82,10 @@ public final class WorldheartPhoenix extends CardImpl { if (game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) { Player player = game.getPlayer(affectedControllerId); if (player != null) { - // can sometimes be cast with base mana cost from grave???? - player.setCastSourceIdWithAlternateMana(sourceId, new ManaCostsImpl<>("{W}{U}{B}{R}{G}"), null); + player.setCastSourceIdWithAlternateMana( + sourceId, new ManaCostsImpl<>("{W}{U}{B}{R}{G}"), null, + MageIdentifier.WorldheartPhoenixAlternateCast + ); return true; } } diff --git a/Mage.Sets/src/mage/cards/x/XandersPact.java b/Mage.Sets/src/mage/cards/x/XandersPact.java index d82be278098..835f99d5cfb 100644 --- a/Mage.Sets/src/mage/cards/x/XandersPact.java +++ b/Mage.Sets/src/mage/cards/x/XandersPact.java @@ -1,5 +1,6 @@ package mage.cards.x; +import mage.MageIdentifier; import mage.abilities.Ability; import mage.abilities.costs.Cost; import mage.abilities.costs.Costs; @@ -36,6 +37,7 @@ public final class XandersPact extends CardImpl { // Each opponent exiles the top card of their library. You may cast spells from among those cards this turn. If you cast a spell this way, pay life equal to that spell's mana value rather than pay its mana cost. this.getSpellAbility().addEffect(new XandersPactExileEffect()); + this.getSpellAbility().setIdentifier(MageIdentifier.XandersPactAlternateCast); } private XandersPact(final XandersPact card) { @@ -120,7 +122,10 @@ class XandersPactCastEffect extends CanPlayCardControllerEffect { Costs newCosts = new CostsImpl<>(); newCosts.add(new PayLifeCost(cardToCheck.getManaValue())); newCosts.addAll(cardToCheck.getSpellAbility().getCosts()); - controller.setCastSourceIdWithAlternateMana(cardToCheck.getId(), null, newCosts); + controller.setCastSourceIdWithAlternateMana( + cardToCheck.getId(), null, newCosts, + MageIdentifier.XandersPactAlternateCast + ); return true; } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/MultipleAsThoughEffects.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/MultipleAsThoughEffects.java new file mode 100644 index 00000000000..c8d72b56cec --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/MultipleAsThoughEffects.java @@ -0,0 +1,81 @@ +package org.mage.test.cards.abilities.other; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Alex-Vasile, Susucr + */ +public class MultipleAsThoughEffects extends CardTestPlayerBase { + + /** + * Reported bug: https://github.com/magefree/mage/issues/8584 + * + * If there are multiple effects which allow a player to cast a spell, + * they should be able to choose which one they whish to use. + */ + @Test + public void ChoosingAlternateCastingMethod() { + setStrictChooseMode(true); + skipInitShuffling(); + + // You may cast creature spells from the top of your library. + addCard(Zone.HAND, playerA, "Vivien, Monsters' Advocate"); + // You may play lands and cast spells from the top of your library. + // If you cast a spell this way, pay life equal to its mana value rather than pay its mana cost. + addCard(Zone.BATTLEFIELD, playerA, "Bolas's Citadel"); + // Random creature card to play with mana value of 3 + addCard(Zone.LIBRARY, playerA, "Abzan Beastmaster",2); + addCard(Zone.LIBRARY, playerA, "Grizzly Bears",1); // This one is drawn. + + addCard(Zone.BATTLEFIELD, playerA, "Forest",5); + // For the "cast from the top" abilities to work, Vivien or Bolas's Citadel + // must be played and not be in battlefield as start. Or else the top of the library will + // not be able to be cast during the test. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Vivien, Monsters' Advocate"); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Abzan Beastmaster",true); + setChoice(playerA, "Vivien"); + checkLife("Vivien not making pay life", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 20); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Abzan Beastmaster"); + setChoice(playerA, "Bolas's"); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Abzan Beastmaster", 2); + assertTappedCount("Forest", true, 3); + assertLife(playerA, 20 - 3); // 3 from casting Abzan Beastmaster with Bolas Citadel + } + + /** + * Reported bug: https://github.com/magefree/mage/issues/2087 + * + * If there are multiple effects which allow a player to cast a spell, + * they should be able to choose which one they whish to use, even if one is single-use. + */ + @Test + public void RisenExecutioner() { + setStrictChooseMode(true); + + // You may cast Risen Executioner from your graveyard if you pay {1} more to cast it for each other creature card in your graveyard. + addCard(Zone.GRAVEYARD, playerA, "Risen Executioner", 2); + addCard(Zone.GRAVEYARD, playerA, "Grizzly Bears", 1); + // During each of your turns, you may cast a Zombie creature spell from your graveyard. + addCard(Zone.BATTLEFIELD, playerA, "Gisa and Geralf"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp",9); // Only enough mana to cast + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Risen Executioner", true); // Should cost {2}{B}{B} since cast with Gisa + setChoice(playerA, "Gisa"); + checkPermanentTapped("Swamp tapped after cast with Gisa and Geralf", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp", true, 4); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Risen Executioner"); // Should cost {3}{B}{B} when cast with own ability, there is another creature in the graveyard + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Risen Executioner", 2); + assertTappedCount("Swamp", true, 4 + 5); + } +} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/adventure/AdventureCardsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/adventure/AdventureCardsTest.java index ae10772bf97..a92a9999e58 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/adventure/AdventureCardsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/adventure/AdventureCardsTest.java @@ -734,6 +734,7 @@ public class AdventureCardsTest extends CardTestPlayerBase { checkExileCount("after exile 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Curious Pair", 1); // play as adventure spell castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Treats to Share"); + setChoice(playerA, "Hostage Taker"); // Not sure why there is an alternative there. No issue with using either. TODO: investigate? waitStackResolved(3, PhaseStep.POSTCOMBAT_MAIN); checkPermanentCount("after play 3", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Food Token", 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/KaradorGhostChieftainTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/KaradorGhostChieftainTest.java index d00cf81270c..5d8162bc03b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/KaradorGhostChieftainTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/KaradorGhostChieftainTest.java @@ -96,11 +96,12 @@ public class KaradorGhostChieftainTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); - addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); addCard(Zone.BATTLEFIELD, playerA, "Island", 3); // // {1}{B}: Target attacking Zombie gains indestructible until end of turn. addCard(Zone.LIBRARY, playerA, "Accursed Horde", 1); // Creature Zombie {3}{B} + addCard(Zone.LIBRARY, playerA, "Carrion Screecher", 1); // Creature Zombie {3}{B} // addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion", 5); // Creature {1}{W} // @@ -119,15 +120,64 @@ public class KaradorGhostChieftainTest extends CardTestPlayerBase { // you play any creatures due to two approve objects checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", true); checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Accursed Horde", true); + checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Carrion Screecher", true); - // cast zombie creature and approves by Karagar + // cast zombie creature and approves by Karador castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Accursed Horde"); setChoice(playerA, "Karador, Ghost Chieftain"); // choose the permitting object - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN); // you can't cast lion due to approving object (Gisa needs zombie) checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", false); checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Accursed Horde", false); + checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Carrion Screecher", true); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + } + + @Test + public void test_castFromGraveyardWithDifferentApproversOtherCast() { + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 6); + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + // + // {1}{B}: Target attacking Zombie gains indestructible until end of turn. + addCard(Zone.LIBRARY, playerA, "Accursed Horde", 1); // Creature Zombie {3}{B} + addCard(Zone.LIBRARY, playerA, "Carrion Screecher", 1); // Creature Zombie {3}{B} + // + addCard(Zone.GRAVEYARD, playerA, "Silvercoat Lion", 5); // Creature {1}{W} + // + // Karador, Ghost Chieftain costs {1} less to cast for each creature card in your graveyard. + // During each of your turns, you may cast one creature card from your graveyard. + addCard(Zone.HAND, playerA, "Karador, Ghost Chieftain");// {5}{B}{G}{W} + // + // When Gisa and Geralf enters the battlefield, put the top four cards of your library into your graveyard. + // During each of your turns, you may cast a Zombie creature card from your graveyard. + addCard(Zone.HAND, playerA, "Gisa and Geralf"); // CREATURE {2}{U}{B} (4/4) + + // prepare spels with same AsThough effects and puts creature to graveyard + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Karador, Ghost Chieftain", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gisa and Geralf"); + + // you play any creatures due to two approve objects + checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", true); + checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Accursed Horde", true); + checkPlayableAbility("before", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Carrion Screecher", true); + + // cast zombie creature and approves by Gisa and Geralf + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Accursed Horde"); + setChoice(playerA, "Gisa and Geralf"); // choose the permitting object + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN); + + // you can't cast lion due to approving object (Gisa needs zombie) + checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Silvercoat Lion", true); + checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Accursed Horde", false); + checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Carrion Screecher", true); setStrictChooseMode(true); setStopAt(3, PhaseStep.BEGIN_COMBAT); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/bro/OneWithTheMultiverseTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/bro/OneWithTheMultiverseTest.java new file mode 100644 index 00000000000..e77c50f952f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/bro/OneWithTheMultiverseTest.java @@ -0,0 +1,370 @@ +package org.mage.test.cards.single.bro; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class OneWithTheMultiverseTest extends CardTestPlayerBase { + + /** One with the Multiverse + * {6}{U}{U} + * Enchantment + * + * You may look at the top card of your library any time. + * You may play lands and cast spells from the top of your library. + * Once during each of your turns, you may cast a spell from your hand or the top of your library without paying its mana cost. + */ + private final String owtm = "One with the Multiverse"; + private final String ogre = "Gray Ogre"; // 2/2 {2}{R} + private final String piker = "Goblin Piker"; // 2/1 {1}{R} + + @Test + public void castFromTopForFree() { + setStrictChooseMode(true); + skipInitShuffling(); + + // The "You may look at the top card of your library any time." + // is not set up properly if starting directly on the battlefield. + // So we do cast it in those tests. + addCard(Zone.HAND, playerA, owtm); + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 8); + + addCard(Zone.LIBRARY, playerA, ogre, 3); + addCard(Zone.HAND, playerA, piker, 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true); + setChoice(playerA, "Without paying manacost"); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, ogre, 1); + assertPermanentCount(playerA, piker, 0); + assertLibraryCount(playerA, ogre, 2); + assertHandCount(playerA, piker, 2); + } + + @Test + public void castFromHandForFree() { + setStrictChooseMode(true); + skipInitShuffling(); + + // The "You may look at the top card of your library any time." + // is not set up properly if starting directly on the battlefield. + // So we do cast it in those tests. + addCard(Zone.HAND, playerA, owtm); + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 8); + + addCard(Zone.LIBRARY, playerA, ogre, 3); + addCard(Zone.HAND, playerA, piker, 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true); + setChoice(playerA, "Without paying manacost"); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, ogre, 0); + assertPermanentCount(playerA, piker, 1); + assertLibraryCount(playerA, ogre, 3); + assertHandCount(playerA, piker, 1); + } + + @Test + public void castFromTopForFreeThenNormalFromTop() { + setStrictChooseMode(true); + skipInitShuffling(); + + // The "You may look at the top card of your library any time." + // is not set up properly if starting directly on the battlefield. + // So we do cast it in those tests. + addCard(Zone.HAND, playerA, owtm); + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11); + + addCard(Zone.LIBRARY, playerA, ogre, 3); + addCard(Zone.HAND, playerA, piker, 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true); + setChoice(playerA, "Without paying manacost"); + checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, ogre, 2); + assertPermanentCount(playerA, piker, 0); + assertLibraryCount(playerA, ogre, 1); + assertHandCount(playerA, piker, 2); + assertTappedCount("Volcanic Island", true, 8 + 3); + } + + @Test + public void castFromTopForFreeThenNormalFromHand() { + setStrictChooseMode(true); + skipInitShuffling(); + + // The "You may look at the top card of your library any time." + // is not set up properly if starting directly on the battlefield. + // So we do cast it in those tests. + addCard(Zone.HAND, playerA, owtm); + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11); + + addCard(Zone.LIBRARY, playerA, ogre, 3); + addCard(Zone.HAND, playerA, piker, 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true); + setChoice(playerA, "Without paying manacost"); + checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, ogre, 1); + assertPermanentCount(playerA, piker, 1); + assertLibraryCount(playerA, ogre, 2); + assertHandCount(playerA, piker, 1); + assertTappedCount("Volcanic Island", true, 8 + 2); + } + + @Test + public void castFromHandForFreeThenNormalFromHand() { + setStrictChooseMode(true); + skipInitShuffling(); + + // The "You may look at the top card of your library any time." + // is not set up properly if starting directly on the battlefield. + // So we do cast it in those tests. + addCard(Zone.HAND, playerA, owtm); + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11); + + addCard(Zone.LIBRARY, playerA, ogre, 3); + addCard(Zone.HAND, playerA, piker, 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true); + setChoice(playerA, "Without paying manacost"); + checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, ogre, 0); + assertPermanentCount(playerA, piker, 2); + assertLibraryCount(playerA, ogre, 3); + assertHandCount(playerA, piker, 0); + assertTappedCount("Volcanic Island", true, 8 + 2); + } + + @Test + public void castFromHandForFreeThenNormalFromTop() { + setStrictChooseMode(true); + skipInitShuffling(); + + // The "You may look at the top card of your library any time." + // is not set up properly if starting directly on the battlefield. + // So we do cast it in those tests. + addCard(Zone.HAND, playerA, owtm); + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11); + + addCard(Zone.LIBRARY, playerA, ogre, 3); + addCard(Zone.HAND, playerA, piker, 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true); + setChoice(playerA, "Without paying manacost"); + checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, ogre, 1); + assertPermanentCount(playerA, piker, 1); + assertLibraryCount(playerA, ogre, 2); + assertHandCount(playerA, piker, 1); + assertTappedCount("Volcanic Island", true, 8 + 3); + } + + @Test + public void castNormalFromTopThenFreeFromHand() { + setStrictChooseMode(true); + skipInitShuffling(); + + // The "You may look at the top card of your library any time." + // is not set up properly if starting directly on the battlefield. + // So we do cast it in those tests. + addCard(Zone.HAND, playerA, owtm); + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11); + + addCard(Zone.LIBRARY, playerA, ogre, 3); + addCard(Zone.HAND, playerA, piker, 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true); + setChoice(playerA, owtm); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true); + setChoice(playerA, "Without paying manacost"); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, ogre, 1); + assertPermanentCount(playerA, piker, 1); + assertLibraryCount(playerA, ogre, 2); + assertHandCount(playerA, piker, 1); + assertTappedCount("Volcanic Island", true, 8 + 3); + } + + @Test + public void castNormalFromTopThenFreeFromTop() { + setStrictChooseMode(true); + skipInitShuffling(); + + // The "You may look at the top card of your library any time." + // is not set up properly if starting directly on the battlefield. + // So we do cast it in those tests. + addCard(Zone.HAND, playerA, owtm); + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11); + + addCard(Zone.LIBRARY, playerA, ogre, 3); + addCard(Zone.HAND, playerA, piker, 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true); + setChoice(playerA, owtm); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true); + setChoice(playerA, "Without paying manacost"); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, ogre, 2); + assertPermanentCount(playerA, piker, 0); + assertLibraryCount(playerA, ogre, 1); + assertHandCount(playerA, piker, 2); + assertTappedCount("Volcanic Island", true, 8 + 3); + } + + @Test + public void castNormalFromHandThenFreeFromHand() { + setStrictChooseMode(true); + skipInitShuffling(); + + // The "You may look at the top card of your library any time." + // is not set up properly if starting directly on the battlefield. + // So we do cast it in those tests. + addCard(Zone.HAND, playerA, owtm); + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11); + + addCard(Zone.LIBRARY, playerA, ogre, 3); + addCard(Zone.HAND, playerA, piker, 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true); + setChoice(playerA, piker); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true); + setChoice(playerA, "Without paying manacost"); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, ogre, 0); + assertPermanentCount(playerA, piker, 2); + assertLibraryCount(playerA, ogre, 3); + assertHandCount(playerA, piker, 0); + assertTappedCount("Volcanic Island", true, 8 + 2); + } + + @Test + public void castNormalFromHandThenFreeFromTop() { + setStrictChooseMode(true); + skipInitShuffling(); + + // The "You may look at the top card of your library any time." + // is not set up properly if starting directly on the battlefield. + // So we do cast it in those tests. + addCard(Zone.HAND, playerA, owtm); + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11); + + addCard(Zone.LIBRARY, playerA, ogre, 3); + addCard(Zone.HAND, playerA, piker, 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, owtm, true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, piker, true); + setChoice(playerA, piker); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", true); + checkPlayableAbility("can cast for free", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ogre, true); + setChoice(playerA, "Without paying manacost"); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Gray Ogre", false); + checkPlayableAbility("can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Goblin Piker", false); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, ogre, 1); + assertPermanentCount(playerA, piker, 1); + assertLibraryCount(playerA, ogre, 2); + assertHandCount(playerA, piker, 1); + assertTappedCount("Volcanic Island", true, 8 + 2); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValkiGodOfLiesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValkiGodOfLiesTest.java index 41348728988..0bc16e33f06 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValkiGodOfLiesTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/ValkiGodOfLiesTest.java @@ -46,6 +46,7 @@ public class ValkiGodOfLiesTest extends CardTestPlayerBase { activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+2: Exile the top card of each player's library."); playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Plains"); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Ephemerate", "Grizzly Bears"); + setChoice(playerA, "Emblem Tibalt"); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); 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 67b879ee42f..b76fd0491b2 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 @@ -3161,22 +3161,22 @@ public class TestPlayer implements Player { } @Override - public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { - computerPlayer.setCastSourceIdWithAlternateMana(sourceId, manaCosts, costs); + public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs, MageIdentifier identifier) { + computerPlayer.setCastSourceIdWithAlternateMana(sourceId, manaCosts, costs, identifier); } @Override - public Set getCastSourceIdWithAlternateMana() { + public Map> getCastSourceIdWithAlternateMana() { return computerPlayer.getCastSourceIdWithAlternateMana(); } @Override - public Map> getCastSourceIdManaCosts() { + public Map>> getCastSourceIdManaCosts() { return computerPlayer.getCastSourceIdManaCosts(); } @Override - public Map> getCastSourceIdCosts() { + public Map>> getCastSourceIdCosts() { return computerPlayer.getCastSourceIdCosts(); } diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index 6c8790e67b0..dca7330c7da 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -1,6 +1,7 @@ package org.mage.test.stub; import mage.ApprovingObject; +import mage.MageIdentifier; import mage.MageObject; import mage.Mana; import mage.abilities.*; @@ -1270,22 +1271,22 @@ public class PlayerStub implements Player { } @Override - public Set getCastSourceIdWithAlternateMana() { + public Map> getCastSourceIdWithAlternateMana() { return null; } @Override - public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { + public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs, MageIdentifier identifier) { } @Override - public Map> getCastSourceIdCosts() { + public Map>> getCastSourceIdCosts() { return null; } @Override - public Map> getCastSourceIdManaCosts() { + public Map>> getCastSourceIdManaCosts() { return null; } diff --git a/Mage/src/main/java/mage/MageIdentifier.java b/Mage/src/main/java/mage/MageIdentifier.java index 81ef3adcebd..1e3fa947d61 100644 --- a/Mage/src/main/java/mage/MageIdentifier.java +++ b/Mage/src/main/java/mage/MageIdentifier.java @@ -4,9 +4,21 @@ package mage; * Used to identify specific actions/events and to be able to assign them to the * correct watcher or other processing. * - * @author LevelX2 + * @author LevelX2, Susucr */ public enum MageIdentifier { + // No special behavior. Cleaner than null as a default. + Default, + + // -------------------------------- // + // spell cast watchers // + // -------------------------------- // + // + // All those are used by a watcher to track spells cast using a matching MageIdentifier way. + // + // e.g. [[Johann, Apprentice Sorcerer]] + // "Once each turn, you may cast an instant or sorcery spell from the top of your library." + // CastFromGraveyardOnceWatcher, CemeteryIlluminatorWatcher, GisaAndGeralfWatcher, @@ -19,7 +31,65 @@ public enum MageIdentifier { WishWatcher, GlimpseTheCosmosWatcher, SerraParagonWatcher, - OneWithTheMultiverseWatcher, + OneWithTheMultiverseWatcher("Without paying manacost"), JohannApprenticeSorcererWatcher, - KaghaShadowArchdruidWatcher + KaghaShadowArchdruidWatcher, + CourtOfLocthwainWatcher("Without paying manacost"), + + // ----------------------------// + // alternate casts // + // ----------------------------// + // + // All those are used to link (cost) modification only when cast + // using an AsThough with the matching MageIdentifier. + // + // e.g. [[Bolas's Citadel]] + // """ + // You may look at the top card of your library any time. + // + // You may play lands and cast spells from the top of your library. + // If you cast a spell this way, pay life equal to its mana value rather than pay its mana cost. + // """ + // + // If there are other ways to cast from the top of the library, then the MageIdentifier being different + // means that the alternate cast won't apply to the other ways to cast. + BolassCitadelAlternateCast, + RisenExectutionerAlternateCast, + DemilichAlternateCast, + DemonicEmbraceAlternateCast, + FalcoSparaPactweaverAlternateCast, + HelbruteAlternateCast, + MaestrosAscendencyAlternateCast, + NashiMoonSagesScionAlternateCast, + RafinnesGuidanceAlternateCast, + RonaSheoldredsFaithfulAlternateCast, + ScourgeOfNelTothAlternateCast, + SqueeDubiousMonarchAlternateCast, + WorldheartPhoenixAlternateCast, + XandersPactAlternateCast; + + /** + * Additional text if there is need to differentiate two very similar effects + * from the same source in the UI. + * See [[Court of Lochtwain]] for an example. + * """ + * At the beginning of your upkeep, exile the top card of target opponent’s library. + * You may play that card for as long as it remains exiled, and mana of any type can be spent to cast it. + * If you're the monarch, until end of turn, you may cast a spell from among cards exiled with + * Court of Locthwain without paying its mana cost. + * """ + */ + private final String additionalText; + + MageIdentifier() { + this(""); + } + + MageIdentifier(String additionalText) { + this.additionalText = additionalText; + } + + public String getAdditionalText() { + return this.additionalText; + } } diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index f68a131b98a..295ff3157f4 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -79,7 +79,7 @@ public abstract class AbilityImpl implements Ability { protected List hints = new ArrayList<>(); protected List icons = new ArrayList<>(); protected Outcome customOutcome = null; // uses for AI decisions instead effects - protected MageIdentifier identifier; // used to identify specific ability (e.g. to match with corresponding watcher) + protected MageIdentifier identifier = MageIdentifier.Default; // used to identify specific ability (e.g. to match with corresponding watcher) protected String appendToRule = null; protected int sourcePermanentTransformCount = 0; diff --git a/Mage/src/main/java/mage/abilities/ActivatedAbility.java b/Mage/src/main/java/mage/abilities/ActivatedAbility.java index c7e848447bd..4cf270557ac 100644 --- a/Mage/src/main/java/mage/abilities/ActivatedAbility.java +++ b/Mage/src/main/java/mage/abilities/ActivatedAbility.java @@ -7,41 +7,57 @@ import mage.constants.TargetController; import mage.constants.TimingRule; import mage.game.Game; -import java.util.UUID; +import java.util.*; /** - * @author BetaSteward_at_googlemail.com + * @author BetaSteward_at_googlemail.com, Susucr */ public interface ActivatedAbility extends Ability { final class ActivationStatus { - private final boolean canActivate; - private final ApprovingObject approvingObject; + // Expected to not be modified after creation. + private final Set approvingObjects; - public ActivationStatus(boolean canActivate, ApprovingObject approvingObject) { - this.canActivate = canActivate; - this.approvingObject = approvingObject; + // If true, the Activation Status will not check if there is an approvingObject. + private final boolean forcedCanActivate; + + public ActivationStatus(ApprovingObject approvingObject) { + this.forcedCanActivate = false; + this.approvingObjects = Collections.singleton(approvingObject); + } + + public ActivationStatus(Set approvingObjects) { + this(false, approvingObjects); + } + + private ActivationStatus(boolean forcedCanActivate, Set approvingObjects) { + this.forcedCanActivate = forcedCanActivate; + this.approvingObjects = new HashSet<>(); + this.approvingObjects.addAll(approvingObjects); } public boolean canActivate() { - return canActivate; - } - - public ApprovingObject getApprovingObject() { - return approvingObject; - } - - public static ActivationStatus getFalse() { - return new ActivationStatus(false, null); + return forcedCanActivate || !approvingObjects.isEmpty(); } /** - * @param approvingObjectAbility ability that allows to activate/use current ability + * @return the set of all approving objects for that ActivationStatus. + * That Set is readonly in spirit, as there might be different parts + * of the engine retrieving info from it. */ - public static ActivationStatus getTrue(Ability approvingObjectAbility, Game game) { - ApprovingObject approvingObject = approvingObjectAbility == null ? null : new ApprovingObject(approvingObjectAbility, game); - return new ActivationStatus(true, approvingObject); + public Set getApprovingObjects() { + return approvingObjects; + } + + private static final ActivationStatus falseInstance = new ActivationStatus(Collections.emptySet()); + + public static ActivationStatus getFalse() { + return falseInstance; + } + + public static ActivationStatus withoutApprovingObject(boolean forcedCanActivate) { + return new ActivationStatus(forcedCanActivate, Collections.emptySet()); } } diff --git a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java index 8f71ee2d7fa..22a1fcc5820 100644 --- a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java @@ -17,6 +17,7 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.CardUtil; +import java.util.Set; import java.util.UUID; /** @@ -179,15 +180,16 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa // timing check //20091005 - 602.5d/602.5e - boolean asInstant; - ApprovingObject approvingObject = game.getContinuousEffects() + Set approvingObjects = game + .getContinuousEffects() .asThough(sourceId, AsThoughEffectType.ACTIVATE_AS_INSTANT, this, controllerId, - game); - asInstant = approvingObject != null; - asInstant |= (timing == TimingRule.INSTANT); + game + ); + boolean asInstant = !approvingObjects.isEmpty() + || (timing == TimingRule.INSTANT); if (!asInstant && !game.canPlaySorcery(playerId)) { return ActivationStatus.getFalse(); } @@ -204,7 +206,13 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa // game.inCheckPlayableState() can't be a help here cause some cards checking activating status, // activatorId must be removed this.activatorId = playerId; - return new ActivationStatus(true, approvingObject); + + if (approvingObjects.isEmpty()) { + return ActivationStatus.withoutApprovingObject(true); + } + else { + return new ActivationStatus(approvingObjects); + } } @Override diff --git a/Mage/src/main/java/mage/abilities/PlayLandAbility.java b/Mage/src/main/java/mage/abilities/PlayLandAbility.java index 8ad0044caca..aef18b74474 100644 --- a/Mage/src/main/java/mage/abilities/PlayLandAbility.java +++ b/Mage/src/main/java/mage/abilities/PlayLandAbility.java @@ -1,11 +1,13 @@ package mage.abilities; import mage.ApprovingObject; +import mage.cards.Card; import mage.constants.AbilityType; import mage.constants.AsThoughEffectType; import mage.constants.Zone; import mage.game.Game; +import java.util.Set; import java.util.UUID; /** @@ -33,15 +35,35 @@ public class PlayLandAbility extends ActivatedAbilityImpl { // no super.canActivate() call - ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); - if (!controlsAbility(playerId, game) && null == approvingObject) { + Set approvingObjects = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); + if (!controlsAbility(playerId, game) && approvingObjects.isEmpty()) { return ActivationStatus.getFalse(); } + //20091005 - 114.2a - return new ActivationStatus(game.isActivePlayer(playerId) - && game.getPlayer(playerId).canPlayLand() - && game.canPlaySorcery(playerId), - approvingObject); + if(!game.isActivePlayer(playerId) + || !game.getPlayer(playerId).canPlayLand() + || !game.canPlaySorcery(playerId)) { + return ActivationStatus.getFalse(); + } + + // TODO: this check may not be required, but removing it require more investigation. + // As of now it is only a way for One with the Multiverse to work. + if (!approvingObjects.isEmpty()) { + Card card = game.getCard(sourceId); + Zone zone = game.getState().getZone(sourceId); + if(card != null && card.isOwnedBy(playerId) && Zone.HAND.match(zone)) { + // Regular casting, to be an alternative to the AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE from hand (e.g. One with the Multiverse): + approvingObjects.add(new ApprovingObject(this, game)); + } + } + + if(approvingObjects.isEmpty()) { + return ActivationStatus.withoutApprovingObject(true); + } + else { + return new ActivationStatus(approvingObjects); + } } @Override diff --git a/Mage/src/main/java/mage/abilities/SpellAbility.java b/Mage/src/main/java/mage/abilities/SpellAbility.java index 0ff4861dda1..4424c905ac5 100644 --- a/Mage/src/main/java/mage/abilities/SpellAbility.java +++ b/Mage/src/main/java/mage/abilities/SpellAbility.java @@ -1,6 +1,7 @@ package mage.abilities; import mage.ApprovingObject; +import mage.MageIdentifier; import mage.MageObject; import mage.abilities.costs.Cost; import mage.abilities.costs.VariableCost; @@ -45,6 +46,7 @@ public class SpellAbility extends ActivatedAbilityImpl { this.spellAbilityType = spellAbilityType; this.spellAbilityCastMode = spellAbilityCastMode; this.addManaCost(cost); + this.setIdentifier(MageIdentifier.Default); setSpellName(); } @@ -97,7 +99,7 @@ public class SpellAbility extends ActivatedAbilityImpl { } } - return null != game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game) // check this first to allow Offering in main phase + return !game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game).isEmpty() // check this first to allow Offering in main phase || timing == TimingRule.INSTANT || object.isInstant(game) || object.hasAbility(FlashAbility.getInstance(), game) @@ -116,13 +118,13 @@ public class SpellAbility extends ActivatedAbilityImpl { } // play from not own hand - ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); - if (approvingObject == null && getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) { + Set approvingObjects = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); + if (approvingObjects.isEmpty() && getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) { // allowed to cast adventures from non-hand? - approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); + approvingObjects = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); } - if (approvingObject == null) { + if (approvingObjects.isEmpty()) { Card card = game.getCard(sourceId); if (!(card != null && card.isOwnedBy(playerId))) { return ActivationStatus.getFalse(); @@ -141,12 +143,26 @@ public class SpellAbility extends ActivatedAbilityImpl { } } + // TODO: this check may not be required, but removing it require more investigation. + // As of now it is only a way for One with the Multiverse to work. + if (!approvingObjects.isEmpty()) { + Card card = game.getCard(sourceId); + Zone zone = game.getState().getZone(sourceId); + if(card != null && card.isOwnedBy(playerId) && Zone.HAND.match(zone)) { + // Regular casting, to be an alternative to the AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE from hand (e.g. One with the Multiverse): + approvingObjects.add(new ApprovingObject(this, game)); + } + } + // 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); if (player != null - && player.getCastSourceIdWithAlternateMana().contains(getSourceId())) { + && player.getCastSourceIdWithAlternateMana() + .getOrDefault(getSourceId(), Collections.emptySet()) + .contains(MageIdentifier.Default) + ) { return ActivationStatus.getFalse(); } } @@ -159,13 +175,20 @@ public class SpellAbility extends ActivatedAbilityImpl { // fused can be called from hand only, so not permitting object allows or other zones checks // see https://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/251926-snapcaster-mage-and-fuse if (game.getState().getZone(splitCard.getId()) == Zone.HAND) { - return new ActivationStatus(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId) - && splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId), null); + return ActivationStatus.withoutApprovingObject(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId) + && splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)); } } return ActivationStatus.getFalse(); } else { - return new ActivationStatus(canChooseTarget(game, playerId), approvingObject); + if(canChooseTarget(game, playerId)) { + if(approvingObjects == null || approvingObjects.isEmpty()) { + return ActivationStatus.withoutApprovingObject(true); + } + else { + return new ActivationStatus(approvingObjects); + } + } } } } diff --git a/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceStaticAbility.java b/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceStaticAbility.java index de4a39b6bdd..cef0a26ddc2 100644 --- a/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceStaticAbility.java +++ b/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceStaticAbility.java @@ -46,7 +46,7 @@ class CastFromGraveyardOnceEffect extends AsThoughEffectImpl { private final FilterCard filter; CastFromGraveyardOnceEffect(FilterCard filter, String text) { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); this.filter = filter; this.staticText = text; } diff --git a/Mage/src/main/java/mage/abilities/common/PassAbility.java b/Mage/src/main/java/mage/abilities/common/PassAbility.java index 39e45b7941b..82d72d5faa8 100644 --- a/Mage/src/main/java/mage/abilities/common/PassAbility.java +++ b/Mage/src/main/java/mage/abilities/common/PassAbility.java @@ -1,5 +1,6 @@ package mage.abilities.common; +import mage.ApprovingObject; import mage.abilities.ActivatedAbilityImpl; import mage.abilities.effects.common.PassEffect; import mage.constants.Zone; @@ -30,7 +31,7 @@ public class PassAbility extends ActivatedAbilityImpl { @Override public ActivationStatus canActivate(UUID playerId, Game game) { - return ActivationStatus.getTrue(this, game); + return new ActivationStatus(new ApprovingObject(this, game)); } @Override diff --git a/Mage/src/main/java/mage/abilities/costs/common/TapSourceCost.java b/Mage/src/main/java/mage/abilities/costs/common/TapSourceCost.java index 35389340721..5a41acfe8e3 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/TapSourceCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/TapSourceCost.java @@ -37,7 +37,7 @@ public class TapSourceCost extends CostImpl { Permanent permanent = game.getPermanent(source.getSourceId()); if (permanent != null) { return !permanent.isTapped() - && (permanent.canTap(game) || null != game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game)); + && (permanent.canTap(game) || !game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game).isEmpty()); } return false; } diff --git a/Mage/src/main/java/mage/abilities/costs/common/UntapSourceCost.java b/Mage/src/main/java/mage/abilities/costs/common/UntapSourceCost.java index 03706c6a437..09ec8fe5b03 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/UntapSourceCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/UntapSourceCost.java @@ -36,7 +36,7 @@ public class UntapSourceCost extends CostImpl { public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { Permanent permanent = game.getPermanent(source.getSourceId()); if (permanent != null) { - return permanent.isTapped() && (permanent.canTap(game) || null != game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game)); + return permanent.isTapped() && (permanent.canTap(game) || game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game).isEmpty()); } return false; } diff --git a/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java b/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java index e3e5beb5556..05e8cbb8e2e 100644 --- a/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/AsThoughEffect.java @@ -41,6 +41,4 @@ public interface AsThoughEffect extends ContinuousEffect { @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 f5f5f5f77e8..09f1c74629b 100644 --- a/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java @@ -1,5 +1,6 @@ package mage.abilities.effects; +import mage.MageIdentifier; import mage.abilities.Ability; import mage.abilities.ActivatedAbility; import mage.cards.Card; @@ -19,23 +20,16 @@ import mage.cards.AdventureCard; public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements AsThoughEffect { protected AsThoughEffectType type; - boolean consumable; public AsThoughEffectImpl(AsThoughEffectType type, Duration duration, Outcome outcome) { - this(type, duration, outcome, false); - } - - public AsThoughEffectImpl(AsThoughEffectType type, Duration duration, Outcome outcome, boolean consumable) { super(duration, outcome); this.type = type; this.effectType = EffectType.ASTHOUGH; - this.consumable = consumable; } protected AsThoughEffectImpl(final AsThoughEffectImpl effect) { super(effect); this.type = effect.type; - this.consumable = effect.consumable; } @Override @@ -84,6 +78,10 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements * @return */ protected boolean allowCardToPlayWithoutMana(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + return allowCardToPlayWithoutMana(objectId, source, affectedControllerId, MageIdentifier.Default, game); + } + + protected boolean allowCardToPlayWithoutMana(UUID objectId, Ability source, UUID affectedControllerId, MageIdentifier identifier, Game game){ Player player = game.getPlayer(affectedControllerId); Card card = game.getCard(objectId); if (card == null || player == null) { @@ -92,33 +90,27 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements if (!card.isLand(game)) { if (card instanceof SplitCard) { Card leftCard = ((SplitCard) card).getLeftHalfCard(); - player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts()); + player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts(), identifier); Card rightCard = ((SplitCard) card).getRightHalfCard(); - player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts()); + player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts(), identifier); } else if (card instanceof ModalDoubleFacedCard) { Card leftCard = ((ModalDoubleFacedCard) card).getLeftHalfCard(); Card rightCard = ((ModalDoubleFacedCard) card).getRightHalfCard(); // some MDFC's are land. IE: sea gate restoration if (!leftCard.isLand(game)) { - player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts()); + player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts(), identifier); } if (!rightCard.isLand(game)) { - player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts()); + player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts(), identifier); } } else if (card instanceof AdventureCard) { Card creatureCard = card.getMainCard(); Card spellCard = ((AdventureCard) card).getSpellCard(); - player.setCastSourceIdWithAlternateMana(creatureCard.getId(), null, creatureCard.getSpellAbility().getCosts()); - player.setCastSourceIdWithAlternateMana(spellCard.getId(), null, spellCard.getSpellAbility().getCosts()); + player.setCastSourceIdWithAlternateMana(creatureCard.getId(), null, creatureCard.getSpellAbility().getCosts(), identifier); + player.setCastSourceIdWithAlternateMana(spellCard.getId(), null, spellCard.getSpellAbility().getCosts(), identifier); } - player.setCastSourceIdWithAlternateMana(objectId, null, card.getSpellAbility().getCosts()); + player.setCastSourceIdWithAlternateMana(objectId, null, card.getSpellAbility().getCosts(), identifier); } return true; } - - @Override - public boolean isConsumable() { - return consumable; - } - } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index d6dc55e0c41..4972061f516 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -1,6 +1,7 @@ package mage.abilities.effects; import mage.ApprovingObject; +import mage.MageIdentifier; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.MageSingleton; @@ -506,9 +507,10 @@ public class ContinuousEffects implements Serializable { * @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 Set of all the ApprovingObject related to that asThough. */ - public ApprovingObject asThough(UUID objectId, AsThoughEffectType type, Ability affectedAbility, UUID controllerId, Game game) { + public Set asThough(UUID objectId, AsThoughEffectType type, Ability affectedAbility, UUID controllerId, Game game) { + Set possibleApprovingObjects = new HashSet<>(); // 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) { @@ -553,18 +555,13 @@ public class ContinuousEffects implements Serializable { idToCheck = objectId; } - Set possibleApprovingObjects = new HashSet<>(); for (AsThoughEffect effect : asThoughEffectsList) { Set abilities = asThoughEffectsMap.get(type).getAbility(effect.getId()); for (Ability ability : abilities) { if (affectedAbility == null) { // 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)); - } else { - return new ApprovingObject(ability, game); - } + possibleApprovingObjects.add(new ApprovingObject(ability, game)); } } else { // applies to one affected ability @@ -575,46 +572,13 @@ public class ContinuousEffects implements Serializable { } if (effect.applies(idToCheck, affectedAbility, ability, game, controllerId)) { - if (effect.isConsumable() && !game.inCheckPlayableState()) { - possibleApprovingObjects.add(new ApprovingObject(ability, game)); - } else { - return new ApprovingObject(ability, game); - } + possibleApprovingObjects.add(new ApprovingObject(ability, game)); } } } } - - if (possibleApprovingObjects.size() == 1) { - return possibleApprovingObjects.iterator().next(); - } else if (possibleApprovingObjects.size() > 1) { - // Select the ability that you use to permit the action - Map keyChoices = new HashMap<>(); - for (ApprovingObject approvingObject : possibleApprovingObjects) { - MageObject mageObject = game.getObject(approvingObject.getApprovingAbility().getSourceId()); - String choiceKey = approvingObject.getApprovingAbility().getId().toString(); - String choiceValue; - if (mageObject == null) { - choiceValue = approvingObject.getApprovingAbility().getRule(); - } else { - choiceValue = mageObject.getIdName() + ": " + approvingObject.getApprovingAbility().getRule(mageObject.getName()); - } - keyChoices.put(choiceKey, choiceValue); - } - Choice choicePermitting = new ChoiceImpl(true); - choicePermitting.setMessage("Choose the permitting object"); - choicePermitting.setKeyChoices(keyChoices); - Player player = game.getPlayer(controllerId); - player.choose(Outcome.Detriment, choicePermitting, game); - for (ApprovingObject approvingObject : possibleApprovingObjects) { - if (approvingObject.getApprovingAbility().getId().toString().equals(choicePermitting.getChoiceKey())) { - return approvingObject; - } - } - } - } - return null; + return possibleApprovingObjects; } /** diff --git a/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneAllEffect.java b/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneAllEffect.java index 265fd5610fb..e891e538766 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneAllEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/asthought/PlayFromNotOwnHandZoneAllEffect.java @@ -1,18 +1,14 @@ package mage.abilities.effects.common.asthought; -import java.util.UUID; - import mage.abilities.Ability; import mage.abilities.effects.AsThoughEffectImpl; 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.filter.FilterCard; import mage.game.Game; +import java.util.UUID; + /** * @author LevelX2 */ @@ -65,7 +61,7 @@ public class PlayFromNotOwnHandZoneAllEffect extends AsThoughEffectImpl { } break; } - return !onlyOwnedCards || card.getOwnerId().equals(source.getControllerId()) + return (!onlyOwnedCards || card.getOwnerId().equals(source.getControllerId())) && filter.match(card, game) && game.getState().getZone(card.getId()).match(fromZone); } diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/EchoEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/EchoEffect.java index b7ba60c4a46..91f30c584ca 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/EchoEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/EchoEffect.java @@ -46,7 +46,7 @@ public class EchoEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); if (controller != null && source.getSourceObjectIfItStillExists(game) != null) { - if (game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.PAY_0_ECHO, source, source.getControllerId(), game) != null) { + if (!game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.PAY_0_ECHO, source, source.getControllerId(), game).isEmpty()) { Cost altCost = new GenericManaCost(0); if (controller.chooseUse(Outcome.Benefit, "Pay {0} instead of the echo cost?", source, game)) { altCost.clearPaid(); diff --git a/Mage/src/main/java/mage/abilities/keyword/EmergeAbility.java b/Mage/src/main/java/mage/abilities/keyword/EmergeAbility.java index 8c9717c920d..28915d3cf99 100644 --- a/Mage/src/main/java/mage/abilities/keyword/EmergeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/EmergeAbility.java @@ -1,5 +1,6 @@ package mage.abilities.keyword; +import mage.ApprovingObject; import mage.Mana; import mage.abilities.SpellAbility; import mage.abilities.costs.mana.ManaCost; @@ -55,7 +56,7 @@ public class EmergeAbility extends SpellAbility { new FilterControlledCreaturePermanent(), this.getControllerId(), this, game)) { ManaCost costToPay = CardUtil.reduceCost(emergeCost.copy(), creature.getManaValue()); if (costToPay.canPay(this, this, this.getControllerId(), game)) { - return ActivationStatus.getTrue(this, game); + return new ActivationStatus(new ApprovingObject(this, game)); } } } diff --git a/Mage/src/main/java/mage/abilities/keyword/FlyingAbility.java b/Mage/src/main/java/mage/abilities/keyword/FlyingAbility.java index d7a7b55ecf2..8bff2f46f6c 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, null, blocker.getControllerId(), game) + || (!game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_DRAGON, null, blocker.getControllerId(), game).isEmpty() && 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 899725125d1..408a4e7b7c8 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, blocker.getControllerId(), source, game, 1) - && null == game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_LANDWALK, null, blocker.getControllerId(), game)) { + && game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_LANDWALK, null, blocker.getControllerId(), game).isEmpty()) { switch (filter.getMessage()) { case "plains": - return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_PLAINSWALK, null, blocker.getControllerId(), game); + return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_PLAINSWALK, null, blocker.getControllerId(), game).isEmpty(); case "island": - return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_ISLANDWALK, null, blocker.getControllerId(), game); + return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_ISLANDWALK, null, blocker.getControllerId(), game).isEmpty(); case "swamp": - return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SWAMPWALK, null, blocker.getControllerId(), game); + return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SWAMPWALK, null, blocker.getControllerId(), game).isEmpty(); case "mountain": - return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_MOUNTAINWALK, null, blocker.getControllerId(), game); + return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_MOUNTAINWALK, null, blocker.getControllerId(), game).isEmpty(); case "forest": - return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_FORESTWALK, null, blocker.getControllerId(), game); + return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_FORESTWALK, null, blocker.getControllerId(), game).isEmpty(); 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 fd95692f59e..113593e5c30 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, null, blocker.getControllerId(), game); + || !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SHADOW, null, blocker.getControllerId(), game).isEmpty(); } @Override diff --git a/Mage/src/main/java/mage/abilities/keyword/SpectacleAbility.java b/Mage/src/main/java/mage/abilities/keyword/SpectacleAbility.java index 7ab9af58d99..4fc7c394bd3 100644 --- a/Mage/src/main/java/mage/abilities/keyword/SpectacleAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/SpectacleAbility.java @@ -1,5 +1,6 @@ package mage.abilities.keyword; +import mage.ApprovingObject; import mage.abilities.SpellAbility; import mage.abilities.costs.mana.ManaCost; import mage.abilities.dynamicvalue.common.OpponentsLostLifeCount; @@ -48,7 +49,7 @@ public class SpectacleAbility extends SpellAbility { public ActivationStatus canActivate(UUID playerId, Game game) { if (OpponentsLostLifeCount.instance.calculate(game, playerId) > 0 && super.canActivate(playerId, game).canActivate()) { - return ActivationStatus.getTrue(this, game); + return new ActivationStatus(new ApprovingObject(this, game)); } return ActivationStatus.getFalse(); } diff --git a/Mage/src/main/java/mage/abilities/keyword/SurgeAbility.java b/Mage/src/main/java/mage/abilities/keyword/SurgeAbility.java index ebc17644aa5..86d5b0aab83 100644 --- a/Mage/src/main/java/mage/abilities/keyword/SurgeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/SurgeAbility.java @@ -1,5 +1,6 @@ package mage.abilities.keyword; +import mage.ApprovingObject; import mage.abilities.SpellAbility; import mage.abilities.costs.mana.ManaCostsImpl; import mage.cards.Card; @@ -54,7 +55,7 @@ public class SurgeAbility extends SpellAbility { if (!player.hasOpponent(playerToCheckId, game)) { if (watcher.getAmountOfSpellsPlayerCastOnCurrentTurn(playerToCheckId) > 0 && super.canActivate(playerId, game).canActivate()) { - return ActivationStatus.getTrue(this, game); + return new ActivationStatus(new ApprovingObject(this, game)); } } } diff --git a/Mage/src/main/java/mage/filter/common/FilterCreatureForCombatBlock.java b/Mage/src/main/java/mage/filter/common/FilterCreatureForCombatBlock.java index 742f07b4a3b..a4f2d379e49 100644 --- a/Mage/src/main/java/mage/filter/common/FilterCreatureForCombatBlock.java +++ b/Mage/src/main/java/mage/filter/common/FilterCreatureForCombatBlock.java @@ -34,7 +34,7 @@ class BlockTappedPredicate implements Predicate { @Override public boolean apply(Permanent input, Game game) { - return !input.isTapped() || null != game.getState().getContinuousEffects().asThough(input.getId(), AsThoughEffectType.BLOCK_TAPPED, null, input.getControllerId(), game); + return !input.isTapped() || !game.getState().getContinuousEffects().asThough(input.getId(), AsThoughEffectType.BLOCK_TAPPED, null, input.getControllerId(), game).isEmpty(); } @Override diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java index c44bae6d0c2..4c091b4f7d5 100644 --- a/Mage/src/main/java/mage/game/combat/CombatGroup.java +++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java @@ -174,8 +174,8 @@ public class CombatGroup implements Serializable, Copyable { Player player = game.getPlayer(defenderAssignsCombatDamage(game) ? defendingPlayerId : attacker.getControllerId()); if ((attacker.getAbilities().containsKey(DamageAsThoughNotBlockedAbility.getInstance().getId()) && player.chooseUse(Outcome.Damage, "Have " + attacker.getLogName() + " assign damage 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).isEmpty()) { // for handling creatures like Thorn Elemental blocked = false; unblockedDamage(first, game); diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index ae4444daaff..e6bd27c7ad4 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -716,7 +716,7 @@ public class GameEvent implements Serializable { if (approvingObject == null) { return false; } - if (identifier == null) { + if (identifier.equals(MageIdentifier.Default)) { return false; } return identifier.equals(approvingObject.getApprovingAbility().getIdentifier()); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index c901c9177eb..62e993a48c3 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -1234,13 +1234,13 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) { if (source != null) { if (abilities.containsKey(ShroudAbility.getInstance().getId())) { - if (null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game)) { + if (game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game).isEmpty()) { return false; } } if (game.getPlayer(this.getControllerId()).hasOpponent(sourceControllerId, game) - && null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) + && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game).isEmpty() && abilities.stream() .filter(HexproofBaseAbility.class::isInstance) .map(HexproofBaseAbility.class::cast) @@ -1416,10 +1416,10 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { // battles can never attack return false; } - ApprovingObject approvingObject = game.getContinuousEffects().asThough( + Set approvingObjects = game.getContinuousEffects().asThough( this.objectId, AsThoughEffectType.ATTACK_AS_HASTE, null, defenderId, game ); - if (hasSummoningSickness() && approvingObject == null) { + if (hasSummoningSickness() && approvingObjects.isEmpty()) { return false; } //20101001 - 508.1c @@ -1435,7 +1435,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { } return !abilities.containsKey(DefenderAbility.getInstance().getId()) - || null != game.getContinuousEffects().asThough(this.objectId, AsThoughEffectType.ATTACK, null, this.getControllerId(), game); + || !game.getContinuousEffects().asThough(this.objectId, AsThoughEffectType.ATTACK, null, this.getControllerId(), game).isEmpty(); } private boolean canAttackCheckRestrictionEffects(UUID defenderId, Game game) { @@ -1455,7 +1455,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { @Override public boolean canBlock(UUID attackerId, Game game) { - if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game) == null || isBattle(game)) { + if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty() || isBattle(game)) { return false; } Permanent attacker = game.getPermanent(attackerId); @@ -1488,7 +1488,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { @Override public boolean canBlockAny(Game game) { - if (tapped && null == game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game)) { + if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty()) { return false; } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index b4914d502b1..b9d66036faa 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -1,9 +1,6 @@ package mage.players; -import mage.ApprovingObject; -import mage.MageItem; -import mage.MageObject; -import mage.Mana; +import mage.*; import mage.abilities.*; import mage.abilities.costs.AlternativeSourceCosts; import mage.abilities.costs.Cost; @@ -1054,13 +1051,28 @@ public interface Player extends MageItem, Copyable { * cost * @param costs alternate other costs you need to pay */ - void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs); + default void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { + setCastSourceIdWithAlternateMana(sourceId, manaCosts, costs, MageIdentifier.Default); + } - Set getCastSourceIdWithAlternateMana(); + /** + * If the next spell cast has the set sourceId, the spell will be cast + * without mana (null) or the mana set to manaCosts instead of its normal + * mana costs. + * + * @param sourceId the source that can be cast without mana + * @param manaCosts alternate ManaCost, null if it can be cast without mana + * cost + * @param costs alternate other costs you need to pay + * @param identifier if not using the MageIdentifier.Default, only apply the alternate mana when ApprovingSource if of that kind. + */ + void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs, MageIdentifier identifier); - Map> getCastSourceIdManaCosts(); + Map> getCastSourceIdWithAlternateMana(); - Map> getCastSourceIdCosts(); + Map>> getCastSourceIdManaCosts(); + + Map>> getCastSourceIdCosts(); void clearCastSourceIdManaCosts(); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index ee32160b4a2..c9573a7a030 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -163,9 +163,12 @@ public abstract class PlayerImpl implements Player, Serializable { // indicates that the spell with the set sourceId can be cast with an alternate mana costs (can also be no mana costs) // support multiple cards with alternative mana cost - protected Set castSourceIdWithAlternateMana = new HashSet<>(); - protected Map> castSourceIdManaCosts = new HashMap<>(); - protected Map> castSourceIdCosts = new HashMap<>(); + // + // A card may be able to cast multiple way with multiple methods. + // The specific MageIdentifier should be checked, before checking null as a fallback. + protected Map> castSourceIdWithAlternateMana = new HashMap<>(); + protected Map>> castSourceIdManaCosts = new HashMap<>(); + protected Map>> castSourceIdCosts = new HashMap<>(); // indicates that the player is in mana payment phase protected boolean payManaMode = false; @@ -279,13 +282,22 @@ public abstract class PlayerImpl implements Player, Serializable { this.bufferTimeLeft = player.getBufferTimeLeft(); this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving; - this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); - for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { - this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + for (Entry> entry : player.getCastSourceIdWithAlternateMana().entrySet()) { + this.castSourceIdWithAlternateMana.put(entry.getKey(), (entry.getValue() == null ? null : new HashSet<>(entry.getValue()))); } - for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { - this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + for (Entry>> entry : player.getCastSourceIdManaCosts().entrySet()) { + this.castSourceIdManaCosts.put(entry.getKey(), new HashMap<>()); + for(Entry> subEntry : entry.getValue().entrySet()) { + this.castSourceIdManaCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy()); + } } + for (Entry>> entry : player.getCastSourceIdCosts().entrySet()) { + this.castSourceIdCosts.put(entry.getKey(), new HashMap<>()); + for(Entry> subEntry : entry.getValue().entrySet()) { + this.castSourceIdCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy()); + } + } + this.payManaMode = player.payManaMode; this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null; for (Designation object : player.designations) { @@ -364,13 +376,20 @@ public abstract class PlayerImpl implements Player, Serializable { this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); this.clearCastSourceIdManaCosts(); - this.castSourceIdWithAlternateMana.clear(); - this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana()); - for (Entry> entry : player.getCastSourceIdManaCosts().entrySet()) { - this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + for (Entry> entry : player.getCastSourceIdWithAlternateMana().entrySet()) { + this.castSourceIdWithAlternateMana.put(entry.getKey(), (entry.getValue() == null ? null : new HashSet<>(entry.getValue()))); } - for (Entry> entry : player.getCastSourceIdCosts().entrySet()) { - this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy())); + for (Entry>> entry : player.getCastSourceIdManaCosts().entrySet()) { + this.castSourceIdManaCosts.put(entry.getKey(), new HashMap<>()); + for(Entry> subEntry : entry.getValue().entrySet()) { + this.castSourceIdManaCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy()); + } + } + for (Entry>> entry : player.getCastSourceIdCosts().entrySet()) { + this.castSourceIdCosts.put(entry.getKey(), new HashMap<>()); + for(Entry> subEntry : entry.getValue().entrySet()) { + this.castSourceIdCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy()); + } } this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null; @@ -636,13 +655,13 @@ public abstract class PlayerImpl implements Player, Serializable { } if (source != null) { if (abilities.containsKey(ShroudAbility.getInstance().getId()) - && null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game)) { + && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game).isEmpty()) { return false; } if (sourceControllerId != null && this.hasOpponent(sourceControllerId, game) - && null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) + && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game).isEmpty() && abilities.stream() .filter(HexproofBaseAbility.class::isInstance) .map(HexproofBaseAbility.class::cast) @@ -1080,25 +1099,37 @@ public abstract class PlayerImpl implements Player, Serializable { } @Override - public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs) { + public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts manaCosts, Costs costs, MageIdentifier identifier) { // cost must be copied for data consistence between game simulations - castSourceIdWithAlternateMana.add(sourceId); - castSourceIdManaCosts.put(sourceId, manaCosts != null ? manaCosts.copy() : null); - castSourceIdCosts.put(sourceId, costs != null ? costs.copy() : null); + castSourceIdWithAlternateMana + .computeIfAbsent(sourceId, k -> new HashSet<>()) + .add(identifier); + + castSourceIdManaCosts + .computeIfAbsent(sourceId, k -> new HashMap<>()) + .put(identifier, manaCosts != null ? manaCosts.copy() : null); + + castSourceIdCosts + .computeIfAbsent(sourceId, k -> new HashMap<>()) + .put(identifier, costs != null ? costs.copy() : null); + + if (identifier == null) { + boolean a = true; + } } @Override - public Set getCastSourceIdWithAlternateMana() { + public Map> getCastSourceIdWithAlternateMana() { return castSourceIdWithAlternateMana; } @Override - public Map> getCastSourceIdCosts() { + public Map>> getCastSourceIdCosts() { return castSourceIdCosts; } @Override - public Map> getCastSourceIdManaCosts() { + public Map>> getCastSourceIdManaCosts() { return castSourceIdManaCosts; } @@ -1187,10 +1218,19 @@ public abstract class PlayerImpl implements Player, Serializable { // ALTERNATIVE COST from dynamic effects // some effects set sourceId to cast without paying mana costs or other costs - if (getCastSourceIdWithAlternateMana().contains(ability.getSourceId())) { + MageIdentifier identifier = approvingObject == null + ? MageIdentifier.Default + : approvingObject.getApprovingAbility().getIdentifier(); + + if (!getCastSourceIdWithAlternateMana().getOrDefault(ability.getSourceId(), Collections.emptySet()).contains(identifier)) { + // identifier has no alternate cast entry for that sourceId, using Default instead. + identifier = MageIdentifier.Default; + } + + if (getCastSourceIdWithAlternateMana().getOrDefault(ability.getSourceId(), Collections.emptySet()).contains(identifier)) { Ability spellAbility = spell.getSpellAbility(); - ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()); - Costs costs = getCastSourceIdCosts().get(ability.getSourceId()); + ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()).get(identifier); + Costs costs = getCastSourceIdCosts().get(ability.getSourceId()).get(identifier); if (alternateCosts == null) { noMana = true; } else { @@ -1273,21 +1313,30 @@ public abstract class PlayerImpl implements Player, Serializable { } } + ApprovingObjectResult approvingResult = chooseApprovingObject( + game, + activationStatus.getApprovingObjects().stream().collect(Collectors.toList()), + false + ); + if (approvingResult.status.equals(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE)) { + return false; // canceled choice of approving object. + } + //20091005 - 305.1 if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()))) { + card.getId(), playLandAbility, playerId, approvingResult.approvingObject))) { // int bookmark = game.bookmarkState(); // land events must return original zone (uses for commander watcher) Zone cardZoneBefore = game.getState().getZone(card.getId()); GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); + card.getId(), playLandAbility, playerId, approvingResult.approvingObject); landEventBefore.setZone(cardZoneBefore); game.fireEvent(landEventBefore); if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) { incrementLandsPlayed(); GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, - card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()); + card.getId(), playLandAbility, playerId, approvingResult.approvingObject); landEventAfter.setZone(cardZoneBefore); game.fireEvent(landEventAfter); @@ -1311,6 +1360,68 @@ public abstract class PlayerImpl implements Player, Serializable { return true; } + private enum ApprovingObjectResultStatus { + CHOSEN, + NO_POSSIBLE_CHOICE, + NOT_REQUIRED_NO_CHOICE, + } + + private class ApprovingObjectResult { + public final ApprovingObjectResultStatus status; + public final ApprovingObject approvingObject; // not null iff status is CHOSEN + + private ApprovingObjectResult(ApprovingObjectResultStatus status, ApprovingObject approvingObject) { + this.status = status; + this.approvingObject = approvingObject; + } + } + + private ApprovingObjectResult chooseApprovingObject(Game game, List possibleApprovingObjects, boolean required) { + // Choosing + if (possibleApprovingObjects.isEmpty()) { + return new ApprovingObjectResult(ApprovingObjectResultStatus.NO_POSSIBLE_CHOICE, null); + } else { + // Select the ability that you use to permit the action + Map keyChoices = new HashMap<>(); + int i = 0; + for (ApprovingObject possibleApprovingObject : possibleApprovingObjects) { + MageObject mageObject = game.getObject(possibleApprovingObject.getApprovingAbility().getSourceId()); + String choiceValue = ""; + MageIdentifier identifier = possibleApprovingObject.getApprovingAbility().getIdentifier(); + if (!identifier.getAdditionalText().isEmpty()) { + choiceValue += identifier.getAdditionalText() + ": "; + } + if (mageObject == null) { + choiceValue += possibleApprovingObject.getApprovingAbility().getRule(); + } else { + choiceValue += mageObject.getIdName() + ": "; + String moreDetails = possibleApprovingObject.getApprovingAbility().getRule(mageObject.getName()); + choiceValue += moreDetails.isEmpty() ? "Cast normally" : moreDetails; + } + keyChoices.put((i++) + "", choiceValue); + } + + int choice = 0; + if (!game.inCheckPlayableState() && keyChoices.size() > 1) { + Choice choicePermitting = new ChoiceImpl(required); + choicePermitting.setMessage("Choose the permitting object"); + choicePermitting.setKeyChoices(keyChoices); + if (canRespond()) { + if (choose(Outcome.Neutral, choicePermitting, game)) { + String choiceKey = choicePermitting.getChoiceKey(); + if (choiceKey != null) { + choice = Integer.parseInt(choiceKey); + } + } else { + return new ApprovingObjectResult(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE, null); + } + } + } + return new ApprovingObjectResult(ApprovingObjectResultStatus.CHOSEN, possibleApprovingObjects.get(choice)); + } + } + + protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) { if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY, ability.getId(), ability, playerId))) { @@ -1463,7 +1574,16 @@ public abstract class PlayerImpl implements Player, Serializable { result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game); break; case SPELL: - result = cast((SpellAbility) ability, game, false, activationStatus.getApprovingObject()); + ApprovingObjectResult approvingResult = chooseApprovingObject( + game, + activationStatus.getApprovingObjects().stream().collect(Collectors.toList()), + false + ); + if (approvingResult.status.equals(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE)) { + return false; // chosen to not approve any AsThough. + } + + result = cast((SpellAbility) ability, game, false, approvingResult.approvingObject); break; default: result = playAbility(ability.copy(), game); @@ -3452,9 +3572,9 @@ public abstract class PlayerImpl implements Player, Serializable { } // ALTERNATIVE COST FROM dynamic effects - if (getCastSourceIdWithAlternateMana().contains(copy.getSourceId())) { - ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()); - Costs costs = getCastSourceIdCosts().get(copy.getSourceId()); + for(MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) { + ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier); + Costs costs = getCastSourceIdCosts().get(copy.getSourceId()).get(identifier); boolean canPutToPlay = true; if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) { @@ -3499,9 +3619,7 @@ public abstract class PlayerImpl implements Player, Serializable { } // Get the ability, if any, which allows for spending many as if it were another color. - // TODO: This needs to be improved to handle multiple approving objects. - // See https://github.com/magefree/mage/issues/8584 - ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(), + Set approvingObjects = game.getContinuousEffects().asThough(ability.getSourceId(), AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); for (Mana mana : abilityOptions) { if (mana.count() == 0) { @@ -3517,7 +3635,7 @@ public abstract class PlayerImpl implements Player, Serializable { // TODO: Describe this // Abilities that let us spend mana as if it were any (or other colors/types) must be handled separately // and can't be incorporated into calculating availableMana since the number of combinations would explode. - if (approvingObject != null && mana.count() <= avail.count()) { + if (!approvingObjects.isEmpty() && mana.count() <= avail.count()) { // TODO: I think this is wrong for spell that require colorless return true; } @@ -3764,7 +3882,7 @@ public abstract class PlayerImpl implements Player, Serializable { // So make it available all the time boolean canUse; if (ability instanceof MorphAbility && object instanceof Card && (game.canPlaySorcery(getId()) - || (null != game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game)))) { + || (!game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game).isEmpty()))) { canUse = canPlayCardByAlternateCost((Card) object, availableMana, playAbility, game); } else { canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions @@ -3843,23 +3961,23 @@ public abstract class PlayerImpl implements Player, Serializable { continue; } - ApprovingObject approvingObject; + Set approvingObjects; if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) { // play hand from non hand zone (except battlefield - you can't play already played permanents) - approvingObject = game.getContinuousEffects().asThough(object.getId(), + approvingObjects = game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); - if (approvingObject == null && isPlaySpell + if (approvingObjects.isEmpty() && isPlaySpell && ((SpellAbility) ability).getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) { - approvingObject = game.getContinuousEffects().asThough(object.getId(), + approvingObjects = game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game); } } else { // other abilities from direct zones - approvingObject = null; + approvingObjects = new HashSet<>(); } - boolean canActivateAsHandZone = approvingObject != null + boolean canActivateAsHandZone = !approvingObjects.isEmpty() || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); boolean possibleToPlay = canActivateAsHandZone && ability.getZone().match(Zone.HAND) @@ -4434,8 +4552,8 @@ 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, null, this.getId(), game)) { + if (!game.getContinuousEffects().asThough(card.getId(), + AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game).isEmpty()) { // 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"; diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index c5b827b9c3b..89d8605e28e 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -2,6 +2,7 @@ package mage.util; import com.google.common.collect.ImmutableList; import mage.ApprovingObject; +import mage.MageIdentifier; import mage.MageObject; import mage.Mana; import mage.abilities.*; @@ -147,7 +148,7 @@ public final class CardUtil { ability.addManaCostsToPay(adjustedCost); } - private static ManaCosts adjustCost(ManaCosts manaCosts, int reduceCount) { + public static ManaCosts adjustCost(ManaCosts manaCosts, int reduceCount) { ManaCosts newCost = new ManaCostsImpl<>(); // nothing to change @@ -1447,8 +1448,8 @@ public final class CardUtil { Costs additionalCostsLeft = leftHalfCard.getSpellAbility().getCosts(); Costs additionalCostsRight = rightHalfCard.getSpellAbility().getCosts(); // set alternative cost and any additional cost - player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsLeft); - player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsRight); + player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsLeft, MageIdentifier.Default); + player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsRight, MageIdentifier.Default); } // allow the card to be cast game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), Boolean.TRUE); @@ -1465,13 +1466,13 @@ public final class CardUtil { // get additional cost if any Costs additionalCostsMDFCLeft = leftHalfCard.getSpellAbility().getCosts(); // set alternative cost and any additional cost - player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsMDFCLeft); + player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsMDFCLeft, MageIdentifier.Default); } if (!rightHalfCard.isLand(game)) { // get additional cost if any Costs additionalCostsMDFCRight = rightHalfCard.getSpellAbility().getCosts(); // set alternative cost and any additional cost - player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsMDFCRight); + player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsMDFCRight, MageIdentifier.Default); } } // allow the card to be cast @@ -1488,8 +1489,8 @@ public final class CardUtil { Costs additionalCostsCreature = creatureCard.getSpellAbility().getCosts(); Costs additionalCostsSpellCard = spellCard.getSpellAbility().getCosts(); // set alternative cost and any additional cost - player.setCastSourceIdWithAlternateMana(creatureCard.getId(), manaCost, additionalCostsCreature); - player.setCastSourceIdWithAlternateMana(spellCard.getId(), manaCost, additionalCostsSpellCard); + player.setCastSourceIdWithAlternateMana(creatureCard.getId(), manaCost, additionalCostsCreature, MageIdentifier.Default); + player.setCastSourceIdWithAlternateMana(spellCard.getId(), manaCost, additionalCostsSpellCard, MageIdentifier.Default); } // allow the card to be cast game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), Boolean.TRUE); @@ -1500,7 +1501,7 @@ public final class CardUtil { if (manaCost != null) { // get additional cost if any Costs additionalCostsNormalCard = card.getSpellAbility().getCosts(); - player.setCastSourceIdWithAlternateMana(card.getMainCard().getId(), manaCost, additionalCostsNormalCard); + player.setCastSourceIdWithAlternateMana(card.getMainCard().getId(), manaCost, additionalCostsNormalCard, MageIdentifier.Default); } // cast it