From c0eab28626ab35011bc3d421c6a32d7e0ac4710b Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:47:39 +0200 Subject: [PATCH] implement [MH3] Primal Prayers ; use choice panel for cast mode choice ; allow some restricted "as thought as it had flash" to work only on matching alternative cast. (#12420) --- Mage.Sets/src/mage/cards/a/Aluren.java | 59 ++-- .../src/mage/cards/a/ApexObservatory.java | 37 +-- Mage.Sets/src/mage/cards/a/AsForetold.java | 38 +-- .../mage/cards/a/AsmodeusTheArchfiend.java | 12 +- .../src/mage/cards/a/AssembleThePlayers.java | 2 +- .../src/mage/cards/d/DarksteelMonolith.java | 25 +- .../src/mage/cards/d/DemonOfFatesDesign.java | 4 +- .../src/mage/cards/e/EtherealValkyrie.java | 13 +- .../cards/j/JohannApprenticeSorcerer.java | 2 +- .../src/mage/cards/l/LaquatussChampion.java | 10 +- Mage.Sets/src/mage/cards/p/PrimalPrayers.java | 105 +++++++ Mage.Sets/src/mage/cards/s/SoulScourge.java | 10 +- Mage.Sets/src/mage/sets/ModernHorizons3.java | 1 + .../cards/abilities/keywords/DashTest.java | 40 ++- .../cards/abilities/keywords/EntwineTest.java | 2 +- .../cards/abilities/keywords/EvokeTest.java | 18 +- .../abilities/keywords/FreerunningTest.java | 4 +- .../KickerWithAnyNumberModesAbilityTest.java | 2 +- .../cards/abilities/keywords/ProwlTest.java | 6 +- .../keywords/SpliceOnArcaneTest.java | 11 +- .../cards/abilities/keywords/WardTest.java | 3 +- .../mana/SpendOnlyManaProducedByTest.java | 4 +- .../counterspell/DisruptingShoalTest.java | 8 +- .../oneshot/counterspell/ForceOfWillTest.java | 17 +- .../cost/additional/CollectEvidenceTest.java | 2 +- ...CastFromHandWithoutPayingManaCostTest.java | 49 +++- .../UseAlternateSourceCostsTest.java | 34 ++- .../cost/modification/DefenseGridTest.java | 5 +- .../test/cards/emblems/EmblemOfCardTest.java | 6 +- .../prevent/RefractionTrapTest.java | 11 +- .../single/cmm/DemonOfFatesDesignTest.java | 62 ++-- .../cards/single/dmc/PrimevalSpawnTest.java | 2 +- .../cards/single/eld/OnceUponATimeTest.java | 23 +- .../cards/single/lrw/MulldrifterTest.java | 41 +-- .../cards/single/mh3/PrimalPrayersTest.java | 169 +++++++++++ .../single/otj/FreestriderCommandoTest.java | 2 +- .../single/otj/SatoruTheInfiltratorTest.java | 4 +- .../cards/single/stx/BalefulMasteryTest.java | 15 +- .../cards/single/wwk/MindbreakTrapTest.java | 4 + .../cards/single/zen/ArchiveTrapTest.java | 2 +- .../test/cards/single/zen/CobraTrapTest.java | 5 + .../cards/single/zen/InfernoTrapTest.java | 8 +- .../java/org/mage/test/player/TestPlayer.java | 4 +- Mage/src/main/java/mage/MageIdentifier.java | 5 +- .../src/main/java/mage/abilities/Ability.java | 20 +- .../main/java/mage/abilities/AbilityImpl.java | 144 ++++++--- .../mage/abilities/ActivatedAbilityImpl.java | 5 +- .../java/mage/abilities/SpellAbility.java | 52 +++- .../CastFromGraveyardOnceEachTurnAbility.java | 7 +- .../common/SpellTransformedAbility.java | 7 +- .../costs/AlternativeCostSourceAbility.java | 160 +++++----- .../costs/AlternativeSourceCosts.java | 25 +- .../costs/AlternativeSourceCostsImpl.java | 34 +-- ...stFromHandWithoutPayingManaCostEffect.java | 9 +- .../mage/abilities/keyword/BlitzAbility.java | 7 +- .../mage/abilities/keyword/EmergeAbility.java | 6 +- .../mage/abilities/keyword/EscapeAbility.java | 6 +- .../mage/abilities/keyword/PlotAbility.java | 5 +- .../abilities/keyword/SpectacleAbility.java | 8 +- .../mage/abilities/keyword/SurgeAbility.java | 6 +- .../abilities/keyword/SuspendAbility.java | 5 +- .../mana/ActivateIfConditionManaAbility.java | 6 - ...mitedTimesPerTurnActivatedManaAbility.java | 6 +- .../main/java/mage/choices/ChoiceImpl.java | 9 +- Mage/src/main/java/mage/game/stack/Spell.java | 6 +- .../java/mage/game/stack/StackAbility.java | 6 +- .../main/java/mage/players/PlayerImpl.java | 276 ++++++++++-------- 67 files changed, 1105 insertions(+), 596 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/p/PrimalPrayers.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/PrimalPrayersTest.java diff --git a/Mage.Sets/src/mage/cards/a/Aluren.java b/Mage.Sets/src/mage/cards/a/Aluren.java index cb896ac3198..716287acfd9 100644 --- a/Mage.Sets/src/mage/cards/a/Aluren.java +++ b/Mage.Sets/src/mage/cards/a/Aluren.java @@ -1,5 +1,6 @@ package mage.cards.a; +import mage.MageIdentifier; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.SourceIsSpellCondition; @@ -35,17 +36,16 @@ public final class Aluren extends CardImpl { private static final FilterCreatureCard filter = new FilterCreatureCard("creature cards with mana value 3 or less"); static { - filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 4)); + filter.add(new ManaValuePredicate(ComparisonType.OR_LESS, 3)); } public Aluren(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}{G}"); - // Any player may play creature cards with converted mana cost 3 or less without paying their mana cost - Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new AlurenRuleEffect()); - // and as though they had flash. - // TODO: This as thought effect may only be used if the creature is cast by the aluren effect + // Any player may play creature cards with converted mana cost 3 or less without paying their mana cost and as though they had flash. + Ability ability = new SimpleStaticAbility(new AlurenRuleEffect()); + ability.setIdentifier(MageIdentifier.AlurenAlternateCast); // Is the link allowing the Flash part to only affect that Alternative Cast Effect effect = new CastAsThoughItHadFlashAllEffect(Duration.WhileOnBattlefield, filter, true); effect.setText("and as though they had flash"); ability.addEffect(effect); @@ -67,18 +67,23 @@ class AlurenRuleEffect extends ContinuousEffectImpl { private static final FilterCreatureCard filter = new FilterCreatureCard("creature cards with mana value 3 or less"); static { - filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 4)); + filter.add(new ManaValuePredicate(ComparisonType.OR_LESS, 3)); } - private final AlternativeCostSourceAbility alternativeCastingCostAbility = new AlternativeCostSourceAbility(null, SourceIsSpellCondition.instance, null, filter, true); + private final AlternativeCostSourceAbility alternativeCastingCostAbility; public AlurenRuleEffect() { - super(Duration.WhileOnBattlefield, Outcome.Detriment); + super(Duration.WhileOnBattlefield, Layer.RulesEffects, SubLayer.NA, Outcome.Detriment); staticText = "Any player may cast creature cards with mana value 3 or less without paying their mana cost"; + alternativeCastingCostAbility = new AlternativeCostSourceAbility( + null, SourceIsSpellCondition.instance, null, filter, true + ); + alternativeCastingCostAbility.setIdentifier(MageIdentifier.AlurenAlternateCast); } private AlurenRuleEffect(final AlurenRuleEffect effect) { super(effect); + this.alternativeCastingCostAbility = effect.alternativeCastingCostAbility.copy(); } @Override @@ -86,34 +91,20 @@ class AlurenRuleEffect extends ContinuousEffectImpl { return new AlurenRuleEffect(this); } - @Override - public void init(Ability source, Game game, UUID activePlayerId) { - super.init(source, game, activePlayerId); - alternativeCastingCostAbility.setSourceId(source.getSourceId()); - } - - @Override - public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) { - Player player = game.getPlayer(playerId); - if (player != null) { - player.getAlternativeSourceCosts().add(alternativeCastingCostAbility); - } - } - return true; - } - return false; - } - @Override public boolean apply(Game game, Ability source) { - return false; + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + alternativeCastingCostAbility.setSourceId(source.getSourceId()); + for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) { + Player player = game.getPlayer(playerId); + if (player != null) { + player.getAlternativeSourceCosts().add(alternativeCastingCostAbility); + } + } + return true; } - @Override - public boolean hasLayer(Layer layer) { - return layer == Layer.RulesEffects; - } } \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/a/ApexObservatory.java b/Mage.Sets/src/mage/cards/a/ApexObservatory.java index 3d3cf06b9e7..8ae6b3ef3b2 100644 --- a/Mage.Sets/src/mage/cards/a/ApexObservatory.java +++ b/Mage.Sets/src/mage/cards/a/ApexObservatory.java @@ -1,11 +1,5 @@ package mage.cards.a; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.stream.Collectors; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.AsEntersBattlefieldAbility; @@ -22,11 +16,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.choices.Choice; import mage.choices.ChoiceCardType; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.Outcome; -import mage.constants.SubLayer; +import mage.constants.*; import mage.filter.common.FilterOwnedCard; import mage.game.ExileZone; import mage.game.Game; @@ -34,8 +24,10 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.CardUtil; +import java.util.*; +import java.util.stream.Collectors; + /** - * * @author jeffwadsworth */ public class ApexObservatory extends CardImpl { @@ -209,20 +201,15 @@ class ApexObservatoryAlternativeCostAbility extends AlternativeCostSourceAbility } @Override - public boolean askToActivateAlternativeCosts(Ability ability, Game game) { - Player controller = game.getPlayer(ability.getControllerId()); - Card apexObservatory = game.getCard(this.getSourceId()); - if (controller != null - && apexObservatory != null) { - if (controller.chooseUse(Outcome.Benefit, "Use " - + apexObservatory.getLogName() + " to pay no mana costs for this spell?", ability, game)) { - wasActivated = super.askToActivateAlternativeCosts(ability, game); - if (wasActivated) { - game.getState().setValue(apexObservatory.getId().toString(), true); - } - } + public boolean activateAlternativeCosts(Ability ability, Game game) { + if (!super.activateAlternativeCosts(ability, game)) { + return false; } - return wasActivated; + Card apexObservatory = game.getCard(this.getSourceId()); + if (apexObservatory != null) { + game.getState().setValue(apexObservatory.getId().toString(), true); + } + return true; } } diff --git a/Mage.Sets/src/mage/cards/a/AsForetold.java b/Mage.Sets/src/mage/cards/a/AsForetold.java index de44fbb1fad..fd39a7dbe1b 100644 --- a/Mage.Sets/src/mage/cards/a/AsForetold.java +++ b/Mage.Sets/src/mage/cards/a/AsForetold.java @@ -1,6 +1,5 @@ package mage.cards.a; -import java.util.UUID; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.SpellAbility; @@ -19,11 +18,12 @@ import mage.counters.CounterType; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; +import mage.util.CardUtil; + +import java.util.UUID; /** - * * @author stravant - * */ public final class AsForetold extends CardImpl { @@ -79,7 +79,7 @@ class SpellWithManaCostLessThanOrEqualToCondition implements Condition { /** * Special AlternativeCostSourceAbility implementation. We wrap the call to - * askToActivateAlternativeCosts in order to tell when the alternative cost is + * activateAlternativeCosts in order to tell when the alternative cost is * used, and mark it as having been used this turn in the watcher */ class AsForetoldAlternativeCost extends AlternativeCostSourceAbility { @@ -101,24 +101,18 @@ class AsForetoldAlternativeCost extends AlternativeCostSourceAbility { } @Override - public boolean askToActivateAlternativeCosts(Ability ability, Game game) { - Player controller = game.getPlayer(ability.getControllerId()); - Permanent asForetold = game.getPermanent(getSourceId()); - if (controller != null - && asForetold != null) { - if (controller.chooseUse(Outcome.Neutral, "Use " - + asForetold.getLogName() + " to pay the alternative cost ?", ability, game)) { - wasActivated = super.askToActivateAlternativeCosts(ability, game); - if (wasActivated) { - game.getState().setValue(asForetold.getId().toString() - + asForetold.getZoneChangeCounter(game) - + asForetold.getTurnsOnBattlefield(), true); - } - } + public boolean activateAlternativeCosts(Ability ability, Game game) { + if (!super.activateAlternativeCosts(ability, game)) { + return false; } - return wasActivated; + Permanent asForetold = game.getPermanent(getSourceId()); + if (asForetold != null) { + game.getState().setValue(asForetold.getId().toString() + + asForetold.getZoneChangeCounter(game) + + asForetold.getTurnsOnBattlefield(), true); + } + return true; } - } /** @@ -149,8 +143,8 @@ class AsForetoldAddAltCostEffect extends ContinuousEffectImpl { if (sourcePermanent != null) { Boolean wasItUsed = (Boolean) game.getState().getValue( sourcePermanent.getId().toString() - + sourcePermanent.getZoneChangeCounter(game) - + sourcePermanent.getTurnsOnBattlefield()); + + sourcePermanent.getZoneChangeCounter(game) + + sourcePermanent.getTurnsOnBattlefield()); // If we haven't used it yet this turn, give the option of using the zero alternative cost if (wasItUsed == null) { int timeCounters = sourcePermanent.getCounters(game).getCount(CounterType.TIME); diff --git a/Mage.Sets/src/mage/cards/a/AsmodeusTheArchfiend.java b/Mage.Sets/src/mage/cards/a/AsmodeusTheArchfiend.java index 904790e561e..c6411c3acbf 100644 --- a/Mage.Sets/src/mage/cards/a/AsmodeusTheArchfiend.java +++ b/Mage.Sets/src/mage/cards/a/AsmodeusTheArchfiend.java @@ -1,6 +1,6 @@ package mage.cards.a; -import java.util.UUID; +import mage.MageIdentifier; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.ActivatedAbilityImpl; @@ -11,9 +11,9 @@ import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.cards.Card; -import mage.constants.*; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.*; import mage.game.ExileZone; import mage.game.Game; import mage.game.events.GameEvent; @@ -21,8 +21,10 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.CardUtil; +import java.util.Set; +import java.util.UUID; + /** - * * @author weirddan455 */ public final class AsmodeusTheArchfiend extends CardImpl { @@ -115,8 +117,8 @@ class AsmodeusTheArchfiendReturnAbility extends ActivatedAbilityImpl { } @Override - public boolean activate(Game game, boolean noMana) { - if (super.activate(game, noMana)) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { + if (super.activate(game, allowedIdentifiers, noMana)) { Permanent sourcePermanent = this.getSourcePermanentIfItStillExists(game); if (sourcePermanent != null) { // Needed to save zcc on activation so it still works if the permanent changes zones in response to the ability being activated. diff --git a/Mage.Sets/src/mage/cards/a/AssembleThePlayers.java b/Mage.Sets/src/mage/cards/a/AssembleThePlayers.java index 76a1d69ff23..4358a66806d 100644 --- a/Mage.Sets/src/mage/cards/a/AssembleThePlayers.java +++ b/Mage.Sets/src/mage/cards/a/AssembleThePlayers.java @@ -104,7 +104,7 @@ class AssembleThePlayersPlayTopEffect extends AsThoughEffectImpl { if (affectedAbility instanceof SpellAbility) { SpellAbility spellAbility = (SpellAbility) affectedAbility; if (spellAbility.getManaCosts().isEmpty() - || !spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + || !spellAbility.spellCanBeActivatedNow(playerId, game).contains(MageIdentifier.Default)) { return false; } Card cardToCheck = spellAbility.getCharacteristics(game); diff --git a/Mage.Sets/src/mage/cards/d/DarksteelMonolith.java b/Mage.Sets/src/mage/cards/d/DarksteelMonolith.java index 5872ad52e7d..0cc187f582f 100644 --- a/Mage.Sets/src/mage/cards/d/DarksteelMonolith.java +++ b/Mage.Sets/src/mage/cards/d/DarksteelMonolith.java @@ -96,22 +96,17 @@ class DarksteelMonolithAlternativeCost extends AlternativeCostSourceAbility { } @Override - public boolean askToActivateAlternativeCosts(Ability ability, Game game) { - Player controller = game.getPlayer(ability.getControllerId()); - Permanent monolith = game.getPermanent(getSourceId()); - if (controller != null - && monolith != null) { - if (controller.chooseUse(Outcome.Neutral, "Use " - + monolith.getLogName() + " to pay the alternative cost?", ability, game)) { - wasActivated = super.askToActivateAlternativeCosts(ability, game); - if (wasActivated) { - game.getState().setValue(monolith.getId().toString() - + monolith.getZoneChangeCounter(game) - + monolith.getTurnsOnBattlefield(), true); - } - } + public boolean activateAlternativeCosts(Ability ability, Game game) { + if (!super.activateAlternativeCosts(ability, game)) { + return false; } - return wasActivated; + Permanent monolith = game.getPermanent(getSourceId()); + if (monolith != null) { + game.getState().setValue(monolith.getId().toString() + + monolith.getZoneChangeCounter(game) + + monolith.getTurnsOnBattlefield(), true); + } + return true; } } diff --git a/Mage.Sets/src/mage/cards/d/DemonOfFatesDesign.java b/Mage.Sets/src/mage/cards/d/DemonOfFatesDesign.java index 0441342f0c8..c58c0001da6 100644 --- a/Mage.Sets/src/mage/cards/d/DemonOfFatesDesign.java +++ b/Mage.Sets/src/mage/cards/d/DemonOfFatesDesign.java @@ -43,6 +43,7 @@ import java.util.UUID; public final class DemonOfFatesDesign extends CardImpl { private static final FilterEnchantmentPermanent filter = new FilterEnchantmentPermanent("another enchantment"); + static { filter.add(AnotherPredicate.instance); } @@ -131,8 +132,7 @@ enum DemonOfFatesDesignCost implements DynamicCost { @Override public String getText(Ability ability, Game game) { - return "Pay " + ability.getManaCosts().manaValue() + " life rather than " - + ability.getManaCosts().getText() + '?'; + return "Pay " + ability.getManaCosts().manaValue() + " life"; } } diff --git a/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java b/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java index 45dbd8023c4..1cc5b9d3b91 100644 --- a/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java +++ b/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java @@ -1,6 +1,5 @@ package mage.cards.e; -import java.util.UUID; import mage.MageInt; import mage.MageObjectReference; import mage.abilities.Ability; @@ -9,13 +8,7 @@ import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.ForetellAbility; -import mage.cards.AdventureCard; -import mage.cards.Card; -import mage.cards.CardImpl; -import mage.cards.CardSetInfo; -import mage.cards.ModalDoubleFacedCard; -import mage.cards.ModalDoubleFacedCardHalf; -import mage.cards.SplitCard; +import mage.cards.*; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; @@ -26,6 +19,8 @@ import mage.players.Player; import mage.target.common.TargetCardInHand; import mage.util.CardUtil; +import java.util.UUID; + /** * @author jeffwadsworth */ @@ -121,7 +116,7 @@ class EtherealValkyrieEffect extends OneShotEffect { game.getState().setValue(exileCard.getMainCard().getId().toString() + "Foretell Cost", creatureCost); game.getState().setValue(exileCard.getMainCard().getId().toString() + "Foretell Split Cost", spellCost); foretellAbility = new ForetellAbility(exileCard, creatureCost, spellCost); - } else if (!exileCard.isLand(game)){ + } else if (!exileCard.isLand(game)) { // normal card String costText = CardUtil.reduceCost(exileCard.getManaCost(), 2).getText(); game.getState().setValue(exileCard.getId().toString() + "Foretell Cost", costText); diff --git a/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java b/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java index 7fa43a84dd5..3a0ed3ac701 100644 --- a/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java +++ b/Mage.Sets/src/mage/cards/j/JohannApprenticeSorcerer.java @@ -106,7 +106,7 @@ class JohannApprenticeSorcererPlayTopEffect extends AsThoughEffectImpl { if (affectedAbility instanceof SpellAbility) { SpellAbility spellAbility = (SpellAbility) affectedAbility; if (spellAbility.getManaCosts().isEmpty() - || !spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + || !spellAbility.spellCanBeActivatedNow(playerId, game).contains(MageIdentifier.Default)) { return false; } Card cardToCheck = spellAbility.getCharacteristics(game); diff --git a/Mage.Sets/src/mage/cards/l/LaquatussChampion.java b/Mage.Sets/src/mage/cards/l/LaquatussChampion.java index 7036a004bbf..28c4a970ad9 100644 --- a/Mage.Sets/src/mage/cards/l/LaquatussChampion.java +++ b/Mage.Sets/src/mage/cards/l/LaquatussChampion.java @@ -1,6 +1,6 @@ package mage.cards.l; -import java.util.UUID; +import mage.MageIdentifier; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; @@ -23,8 +23,10 @@ import mage.target.TargetPlayer; import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; +import java.util.Set; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class LaquatussChampion extends CardImpl { @@ -68,8 +70,8 @@ class LaquatussChampionEntersBattlefieldTriggeredAbility extends EntersBattlefie } @Override - public boolean activate(Game game, boolean noMana) { - if (super.activate(game, noMana)) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { + if (super.activate(game, allowedIdentifiers, noMana)) { Player player = game.getPlayer(getFirstTarget()); if (player != null) { String key = CardUtil.getCardZoneString("targetPlayer", getSourceId(), game); diff --git a/Mage.Sets/src/mage/cards/p/PrimalPrayers.java b/Mage.Sets/src/mage/cards/p/PrimalPrayers.java new file mode 100644 index 00000000000..2173c819159 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PrimalPrayers.java @@ -0,0 +1,105 @@ +package mage.cards.p; + +import mage.MageIdentifier; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.SourceIsSpellCondition; +import mage.abilities.costs.AlternativeCostSourceAbility; +import mage.abilities.costs.common.PayEnergyCost; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.continuous.CastAsThoughItHadFlashAllEffect; +import mage.abilities.effects.common.counter.GetEnergyCountersControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.game.Game; +import mage.players.Player; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class PrimalPrayers extends CardImpl { + + private static final FilterCreatureCard filter = new FilterCreatureCard("creature cards with mana value 3 or less"); + + static { + filter.add(new ManaValuePredicate(ComparisonType.OR_LESS, 3)); + } + + public PrimalPrayers(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}{G}"); + + // When Primal Prayers enters the battlefield, you get {E}{E}. + this.addAbility(new EntersBattlefieldTriggeredAbility(new GetEnergyCountersControllerEffect(2))); + + // You may cast creature spells with mana value 3 or less by paying {E} rather than paying their mana costs. If you cast a spell this way, you may cast it as though it had flash. + Ability ability = new SimpleStaticAbility(new PrimalPrayersCastEffect()); + ability.setIdentifier(MageIdentifier.PrimalPrayersAlternateCast); // Is the link allowing the Flash part to only affect that Alternative Cast + Effect effect = new CastAsThoughItHadFlashAllEffect(Duration.WhileOnBattlefield, filter, false); + effect.setText("and as though they had flash"); + ability.addEffect(effect); + this.addAbility(ability); + } + + private PrimalPrayers(final PrimalPrayers card) { + super(card); + } + + @Override + public PrimalPrayers copy() { + return new PrimalPrayers(this); + } +} + +// Very close to Aluren +class PrimalPrayersCastEffect extends ContinuousEffectImpl { + + private static final FilterCreatureCard filter = new FilterCreatureCard("creature cards with mana value 3 or less"); + + static { + filter.add(new ManaValuePredicate(ComparisonType.OR_LESS, 3)); + } + + private final AlternativeCostSourceAbility alternativeCastingCostAbility; + + public PrimalPrayersCastEffect() { + super(Duration.WhileOnBattlefield, Layer.RulesEffects, SubLayer.NA, Outcome.Detriment); + staticText = "you may cast creature cards with mana value 3 or less by paying {E} rather than paying their mana costs"; + alternativeCastingCostAbility = new AlternativeCostSourceAbility( + new PayEnergyCost(1), SourceIsSpellCondition.instance, null, filter, true + ); + alternativeCastingCostAbility.setIdentifier(MageIdentifier.PrimalPrayersAlternateCast); + } + + private PrimalPrayersCastEffect(final PrimalPrayersCastEffect effect) { + super(effect); + this.alternativeCastingCostAbility = effect.alternativeCastingCostAbility.copy(); + } + + @Override + public PrimalPrayersCastEffect copy() { + return new PrimalPrayersCastEffect(this); + } + + @Override + public void init(Ability source, Game game, UUID activePlayerId) { + super.init(source, game, activePlayerId); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + alternativeCastingCostAbility.setSourceId(source.getSourceId()); + controller.getAlternativeSourceCosts().add(alternativeCastingCostAbility); + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SoulScourge.java b/Mage.Sets/src/mage/cards/s/SoulScourge.java index b5a428260f2..283d3f3adf1 100644 --- a/Mage.Sets/src/mage/cards/s/SoulScourge.java +++ b/Mage.Sets/src/mage/cards/s/SoulScourge.java @@ -1,6 +1,6 @@ package mage.cards.s; -import java.util.UUID; +import mage.MageIdentifier; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; @@ -20,8 +20,10 @@ import mage.target.TargetPlayer; import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; +import java.util.Set; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class SoulScourge extends CardImpl { @@ -66,8 +68,8 @@ class SoulScourgeEntersBattlefieldTriggeredAbility extends EntersBattlefieldTrig } @Override - public boolean activate(Game game, boolean noMana) { - if (super.activate(game, noMana)) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { + if (super.activate(game, allowedIdentifiers, noMana)) { Player player = game.getPlayer(getFirstTarget()); if (player != null) { String key = CardUtil.getCardZoneString("targetPlayer", getSourceId(), game); diff --git a/Mage.Sets/src/mage/sets/ModernHorizons3.java b/Mage.Sets/src/mage/sets/ModernHorizons3.java index aee850f48b1..ba2489554fd 100644 --- a/Mage.Sets/src/mage/sets/ModernHorizons3.java +++ b/Mage.Sets/src/mage/sets/ModernHorizons3.java @@ -228,6 +228,7 @@ public final class ModernHorizons3 extends ExpansionSet { cards.add(new SetCardInfo("Polluted Delta", 224, Rarity.RARE, mage.cards.p.PollutedDelta.class)); cards.add(new SetCardInfo("Powerbalance", 131, Rarity.RARE, mage.cards.p.Powerbalance.class)); cards.add(new SetCardInfo("Priest of Titania", 286, Rarity.UNCOMMON, mage.cards.p.PriestOfTitania.class)); + cards.add(new SetCardInfo("Primal Prayers", 166, Rarity.RARE, mage.cards.p.PrimalPrayers.class)); cards.add(new SetCardInfo("Propagator Drone", 167, Rarity.UNCOMMON, mage.cards.p.PropagatorDrone.class)); cards.add(new SetCardInfo("Proud Pack-Rhino", 41, Rarity.UNCOMMON, mage.cards.p.ProudPackRhino.class)); cards.add(new SetCardInfo("Psychic Frog", 199, Rarity.RARE, mage.cards.p.PsychicFrog.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DashTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DashTest.java index 6444a3d2ed6..0f46ba355d6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DashTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DashTest.java @@ -5,6 +5,7 @@ import mage.abilities.keyword.HasteAbility; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -13,12 +14,12 @@ import org.mage.test.serverside.base.CardTestPlayerBase; * function while the card with dash is on the stack, one of which may * create a delayed triggered ability, and a static ability that functions * while the object with dash is on the battlefield. - * + *

* “Dash [cost]” means “You may cast this card by paying [cost] rather that * its mana cost,” “If this spell's dash cost was paid, return the permanent this * spell becomes to its owner's hand at the beginning of the next end step,” * and “As long as this permanent's dash cost was paid, it has haste.” - * + *

* Paying a card's dash cost follows the rules for paying alternative costs * in rules 601.2b and 601.2e–g. * @@ -29,17 +30,19 @@ public class DashTest extends CardTestPlayerBase { * Screamreach Brawler * Creature — Orc Berserker 2/3 * {2}{R} - * + *

* Dash {1}{R} (You may cast this spell for its dash cost. If you do, it gains haste, and it's returned * from the battlefield to its owner's hand at the beginning of the next end step.) */ @Test public void testDash() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); addCard(Zone.HAND, playerA, "Screamreach Brawler"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Screamreach Brawler"); - setChoice(playerA, true); + setChoice(playerA, "Cast with Dash alternative cost: {1}{R} (source: Screamreach Brawler"); attack(1, playerA, "Screamreach Brawler"); setStopAt(2, PhaseStep.UNTAP); @@ -53,11 +56,13 @@ public class DashTest extends CardTestPlayerBase { @Test public void testNoDash() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); addCard(Zone.HAND, playerA, "Screamreach Brawler"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Screamreach Brawler"); - setChoice(playerA, false); + setChoice(playerA, TestPlayer.CHOICE_NORMAL_COST); checkPlayableAbility("attack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "attack: Scream", false); setStopAt(2, PhaseStep.UNTAP); @@ -72,18 +77,20 @@ public class DashTest extends CardTestPlayerBase { } /** - * Also dash returns creatures to your hand at end of turn even if they died - * that turn. + * If a dashed creature dies, it is not returned to hand. */ @Test public void testDashedCreatureDiesInCombat() { - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); // enough for Dash, can't cast normal way. addCard(Zone.HAND, playerA, "Screamreach Brawler"); // 2/3 addCard(Zone.BATTLEFIELD, playerB, "Geist of the Moors", 1); // 3/1 castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Screamreach Brawler"); - setChoice(playerA, true); + setChoice(playerA, "Cast with Dash alternative cost: {1}{R} (source: Screamreach Brawler"); + attack(1, playerA, "Screamreach Brawler"); block(1, playerB, "Geist of the Moors", "Screamreach Brawler"); @@ -103,14 +110,17 @@ public class DashTest extends CardTestPlayerBase { */ @Test public void testDashedCreatureDiesInCombatAndIsLaterRecast() { - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); // enough for Dash, can't cast normal way. addCard(Zone.HAND, playerA, "Screamreach Brawler"); // 2/3 castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Screamreach Brawler"); - setChoice(playerA, true); + setChoice(playerA, "Cast with Dash alternative cost: {1}{R} (source: Screamreach Brawler"); attack(1, playerA, "Screamreach Brawler"); castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Screamreach Brawler"); + setChoice(playerA, "Cast with Dash alternative cost: {1}{R} (source: Screamreach Brawler"); setStopAt(3, PhaseStep.BEGIN_COMBAT); execute(); @@ -122,13 +132,15 @@ public class DashTest extends CardTestPlayerBase { @Test public void testWarbringerCostReduction() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); addCard(Zone.BATTLEFIELD, playerA, "Warbringer"); addCard(Zone.HAND, playerA, "Warbringer"); setStrictChooseMode(true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Warbringer"); - setChoice(playerA, true); + setChoice(playerA, "Cast with Dash alternative cost: {2}{R} (source: Warbringer"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -138,13 +150,15 @@ public class DashTest extends CardTestPlayerBase { @Test public void testRegularCostReduction() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); addCard(Zone.BATTLEFIELD, playerA, "Ruby Medallion"); addCard(Zone.HAND, playerA, "Screamreach Brawler"); setStrictChooseMode(true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Screamreach Brawler"); - setChoice(playerA, true); + setChoice(playerA, "Cast with Dash alternative cost: {1}{R} (source: Screamreach Brawler"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java index 190f7e878a1..04eef3e7190 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java @@ -130,7 +130,7 @@ public class EntwineTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, true); // cast for free + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); // cast for free setChoice(playerA, true); // use Entwine addTarget(playerA, "Balduvian Bears"); addTarget(playerA, playerA); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EvokeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EvokeTest.java index ab7f687ec5b..015f2f4c9a2 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EvokeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EvokeTest.java @@ -8,7 +8,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ @@ -30,6 +29,8 @@ public class EvokeTest extends CardTestPlayerBase { @Test public void testCreatureComesIntoPlay() { + setStrictChooseMode(true); + // Check that Lion goes to graveyard from evoke ability // Check that evoke does not trigger again to sacrifice Shriekmaw if it's exhumed @@ -40,15 +41,14 @@ public class EvokeTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shriekmaw"); - setChoice(playerA, true); - setChoice(playerA, "When {this} enters the battlefield, destroy"); //Stack triggers - addTarget(playerA, "Silvercoat Lion"); // Destroy + setChoice(playerA, "Cast with Evoke alternative cost: {1}{B} (source: Shriekmaw"); + setChoice(playerA, "When this permanent enters the battlefield, if its evoke cost was paid, its controller sacrifices it."); // stack triggers + addTarget(playerA, "Silvercoat Lion"); // choice for Shriekmaw Destroy trigger castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Exhume"); - addTarget(playerA, "Shriekmaw"); - addTarget(playerB, "Silvercoat Lion"); //Return - - addTarget(playerA, "Silvercoat Lion"); // Destroy + addTarget(playerB, "Silvercoat Lion"); // Exhume choice + addTarget(playerA, "Shriekmaw"); // Exhume choice + addTarget(playerA, "Silvercoat Lion"); // choice for Shriekmaw Destroy trigger setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -73,7 +73,7 @@ public class EvokeTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mulldrifter"); - setChoice(playerA, true); + setChoice(playerA, "Cast with Evoke alternative cost: {2}{U} (source: Mulldrifter"); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); setChoice(playerA, "When {this} enters the battlefield, draw"); //Stack triggers diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/FreerunningTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/FreerunningTest.java index 6e5558a1ee6..f4325b6ce1f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/FreerunningTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/FreerunningTest.java @@ -37,8 +37,8 @@ public class FreerunningTest extends CardTestCommanderDuelBase { attack(1, playerA, poisoner, playerB); - setChoice(playerA, true); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, vision); + setChoice(playerA, "Cast with Freerunning alternative cost: {1}{U} (source: Eagle Vision"); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -59,8 +59,8 @@ public class FreerunningTest extends CardTestCommanderDuelBase { attack(1, playerA, goblin, playerB); - setChoice(playerA, true); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, vision); + setChoice(playerA, "Cast with Freerunning alternative cost: {1}{U} (source: Eagle Vision"); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerWithAnyNumberModesAbilityTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerWithAnyNumberModesAbilityTest.java index 9180767592e..d42dd419e14 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerWithAnyNumberModesAbilityTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerWithAnyNumberModesAbilityTest.java @@ -135,7 +135,7 @@ public class KickerWithAnyNumberModesAbilityTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inscription of Abundance"); - setChoice(playerA, true); // use free cast + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); // use free cast setChoice(playerA, true); // use kicker setModeChoice(playerA, "2"); setModeChoice(playerA, "1"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ProwlTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ProwlTest.java index 21e298b2272..449c135a28d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ProwlTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ProwlTest.java @@ -29,7 +29,7 @@ public class ProwlTest extends CardTestPlayerBase { checkPlayableAbility("must play", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Auntie's Snitch", true); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Auntie's Snitch"); - setChoice(playerA, true); // choosing to pay prowl cost + setChoice(playerA, "Cast with Prowl alternative cost: {1}{B} (source: Auntie's Snitch"); // choosing to pay prowl cost setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -64,7 +64,7 @@ public class ProwlTest extends CardTestPlayerBase { checkPlayableAbility("must play", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Auntie's Snitch", true); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Auntie's Snitch"); // should only cost {B} with Warchief discount - setChoice(playerA, true); // choosing to pay prowl cost + setChoice(playerA, "Cast with Prowl alternative cost: {1}{B} (source: Auntie's Snitch"); // choosing to pay prowl cost setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -94,7 +94,7 @@ public class ProwlTest extends CardTestPlayerBase { checkPlayableAbility("must play", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Thrasta", true); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Thrasta, Tempest's Roar"); - setChoice(playerA, true); // choosing to pay prowl cost + setChoice(playerA, "Cast with Prowl alternative cost: {2}{R} (source: Thrasta, Tempest's Roar"); // choosing to pay prowl cost setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpliceOnArcaneTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpliceOnArcaneTest.java index 0741181454e..e0ae455e944 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpliceOnArcaneTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpliceOnArcaneTest.java @@ -19,6 +19,8 @@ public class SpliceOnArcaneTest extends CardTestPlayerBase { */ @Test public void testSpliceThroughTheBreach() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); // Sorcery - Arcane {R} // Lava Spike deals 3 damage to target player. @@ -49,6 +51,7 @@ public class SpliceOnArcaneTest extends CardTestPlayerBase { @Test public void testSpliceTorrentOfStone() { + setStrictChooseMode(true); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); // Sorcery - Arcane {R} @@ -64,8 +67,9 @@ public class SpliceOnArcaneTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lava Spike", playerB); // activate splice: yes -> card with splice ability -> new target for spliced ability setChoice(playerA, true); + setChoice(playerA, "Mountain", 2); // sacrifice 2 Mountain addTarget(playerA, "Torrent of Stone"); - // Silvercoat Lion is auto-chosen is only possible target + addTarget(playerA, "Silvercoat Lion"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -91,6 +95,8 @@ public class SpliceOnArcaneTest extends CardTestPlayerBase { */ @Test public void testSpliceThroughTheBreach2() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); // You may exile a green card with converted mana cost X from your hand rather than pay Nourishing Shoal's mana cost. // You gain X life. @@ -104,6 +110,8 @@ public class SpliceOnArcaneTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nourishing Shoal"); // activate splice: yes -> card with splice ability -> new target for spliced ability setChoice(playerA, true); + setChoice(playerA, "Cast with alternative cost: Exile a green card with mana value X from your hand (source: Nourishing Shoal"); + setChoice(playerA, "Giant Growth"); // Exiled for Shoal alternative cost addTarget(playerA, "Through the Breach"); setChoice(playerA, "Silvercoat Lion"); // target for spliced ability: put from hand to battlefield @@ -154,6 +162,7 @@ public class SpliceOnArcaneTest extends CardTestPlayerBase { @Test @Ignore public void testCounteredBecauseOfNoLegalTarget() { + setStrictChooseMode(true); // TODO: rewrite test, it's wrong and misleading-- user report about Griselbrand was destroyed by Terminate after splice announce, but tests don't use it at all (Griselbrand legal target all the time) addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/WardTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/WardTest.java index ba056f35c91..294daf47e86 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/WardTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/WardTest.java @@ -6,7 +6,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author weirddan455 */ public class WardTest extends CardTestPlayerBase { @@ -19,7 +18,7 @@ public class WardTest extends CardTestPlayerBase { setStrictChooseMode(true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Solitude"); - setChoice(playerA, "Yes"); // Use alternate casting cost + setChoice(playerA, "Cast with Evoke alternative cost: Exile a white card from your hand (source: Solitude"); setChoice(playerA, "Healer's Hawk"); setChoice(playerA, "When {this} enters the battlefield, exile up to one other target creature"); // Put exile trigger on the stack first (evoke trigger will resolve first) addTarget(playerA, "Waterfall Aerialist"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/mana/SpendOnlyManaProducedByTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/mana/SpendOnlyManaProducedByTest.java index 40d5d8ca1f5..9a37cf6c7ae 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/mana/SpendOnlyManaProducedByTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/mana/SpendOnlyManaProducedByTest.java @@ -181,7 +181,7 @@ public class SpendOnlyManaProducedByTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rapaciousDragon); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, securityRhox); - setChoice(playerA, true); // pay alternative cost + setChoice(playerA, "Cast with alternative cost: {R}{G} (source: Security Rhox"); // pay alternative cost setChoice(playerA, "Red"); setChoice(playerA, "Green"); @@ -199,7 +199,7 @@ public class SpendOnlyManaProducedByTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Taiga", 2); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, securityRhox); - setChoice(playerA, true); // pay alternative cost + setChoice(playerA, "Cast with alternative cost: {R}{G} (source: Security Rhox"); // pay alternative cost setStrictChooseMode(true); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/counterspell/DisruptingShoalTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/counterspell/DisruptingShoalTest.java index 98ff75d9886..20169ad777b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/counterspell/DisruptingShoalTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/counterspell/DisruptingShoalTest.java @@ -87,7 +87,7 @@ public class DisruptingShoalTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pillarfield Ox"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Disrupting Shoal", "Pillarfield Ox", "Pillarfield Ox"); - setChoice(playerB, true); // use alternate costs + setChoice(playerB, "Cast with alternative cost: Exile a blue card with mana value X from your hand (source: Disrupting Shoal"); // use alternate costs setChoice(playerB, "Mistfire Adept"); // pay to cast Mistfire Adept (CMC = 4) // rules: 202.3e When calculating the converted mana cost of an object with an {X} in its mana cost, @@ -136,7 +136,7 @@ public class DisruptingShoalTest extends CardTestPlayerBase { // try to pay by split card, but can't counter -- X <> bear's cmc castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Disrupting Shoal", "Grizzly Bears", "Grizzly Bears"); - setChoice(playerB, true); // use alternative cost + setChoice(playerB, "Cast with alternative cost: Exile a blue card with mana value X from your hand (source: Disrupting Shoal"); // use alternate costs setChoice(playerB, "Far // Away"); // pay by card (cmc = 5, so X = 5 too) setStrictChooseMode(true); @@ -167,7 +167,7 @@ public class DisruptingShoalTest extends CardTestPlayerBase { // try to pay by split card, but can't counter -- X <> centaur's cmc castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Disrupting Shoal", "Centaur Courser", "Centaur Courser"); - setChoice(playerB, true); // use alternative cost + setChoice(playerB, "Cast with alternative cost: Exile a blue card with mana value X from your hand (source: Disrupting Shoal"); // use alternate costs setChoice(playerB, "Far // Away"); // pay by card (cmc = 5, so X = 5 too) setStrictChooseMode(true); @@ -198,7 +198,7 @@ public class DisruptingShoalTest extends CardTestPlayerBase { // try to pay by split card and it works castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Disrupting Shoal", "Air Elemental", "Air Elemental"); - setChoice(playerB, true); // use alternative cost + setChoice(playerB, "Cast with alternative cost: Exile a blue card with mana value X from your hand (source: Disrupting Shoal"); // use alternate costs setChoice(playerB, "Far // Away"); // pay by card (cmc = 5, so X = 5 too) setStrictChooseMode(true); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/counterspell/ForceOfWillTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/counterspell/ForceOfWillTest.java index 4b9852531ce..e2146937ed9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/counterspell/ForceOfWillTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/counterspell/ForceOfWillTest.java @@ -21,22 +21,25 @@ public class ForceOfWillTest extends CardTestPlayerBase { */ @Test public void testWithBlueCardsInHand() { + setStrictChooseMode(true); + addCard(Zone.HAND, playerA, "Thoughtseize"); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); - + addCard(Zone.HAND, playerB, "Force of Will"); addCard(Zone.HAND, playerB, "Remand", 2); // blue cards to pay force of will addCard(Zone.BATTLEFIELD, playerB, "Island", 2); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thoughtseize", playerB); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Force of Will", "Thoughtseize"); - playerB.addChoice("Yes"); // use alternate costs - + setChoice(playerB, "Cast with alternative cost: Pay 1 life, Exile a blue card from your hand (source: Force of Will"); + setChoice(playerB, "Remand"); + setStopAt(1, PhaseStep.CLEANUP); execute(); - assertLife(playerA, 20); + assertLife(playerA, 20); assertLife(playerB, 19); // losing 1 from Force of Will assertHandCount(playerA, 0); @@ -45,7 +48,7 @@ public class ForceOfWillTest extends CardTestPlayerBase { assertGraveyardCount(playerB, 1); // Force of Will assertExileCount("Remand", 1); // one Remand (cost from Force of Will) } - + /** * Test that Force of Will can't be played with alternate casting costs * if no blue card is in hand and not enough mana available diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/CollectEvidenceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/CollectEvidenceTest.java index 2a8ef06e381..2e608ce4130 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/CollectEvidenceTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/CollectEvidenceTest.java @@ -307,7 +307,7 @@ public class CollectEvidenceTest extends CardTestPlayerBase { addCard(Zone.GRAVEYARD, playerA, ogre, 4); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Colossal Dreadmaw"); - setChoice(playerA, true); // use alternative cast from unraveler + setChoice(playerA, "Cast with alternative cost: Collect evidence 10 (source: Conspiracy Unraveler"); // use alternative cast from unraveler setChoice(playerA, ogre, 4); // pay for collect evidence setStrictChooseMode(true); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java index 4872925abde..10f2bc5eb3c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java @@ -10,12 +10,15 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { @Test public void testSpellNoCost() { + setStrictChooseMode(true); + // You may cast nonland cards from your hand without paying their mana costs. addCard(Zone.BATTLEFIELD, playerA, "Omniscience", 1); addCard(Zone.HAND, playerA, "Gray Ogre", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gray Ogre"); + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); setStopAt(1, PhaseStep.END_TURN); execute(); @@ -32,6 +35,8 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { */ @Test public void testSpellHasCostIfCastFromGraveyard() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Omniscience", 1); addCard(Zone.BATTLEFIELD, playerA, "Haakon, Stromgald Scourge", 1); @@ -55,6 +60,8 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { */ @Test public void testMonocoloredHybridMana() { + setStrictChooseMode(true); + // You may cast nonland cards from your hand without paying their mana costs. addCard(Zone.BATTLEFIELD, playerA, "Omniscience", 1); @@ -63,6 +70,8 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Beseech the Queen", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Beseech the Queen"); + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); + addTarget(playerA, "Mountain"); setStopAt(1, PhaseStep.END_TURN); execute(); @@ -73,12 +82,15 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { @Test public void testColorlessMana() { + setStrictChooseMode(true); + // You may cast nonland cards from your hand without paying their mana costs. addCard(Zone.BATTLEFIELD, playerA, "Omniscience", 1); addCard(Zone.HAND, playerA, "Reality Smasher", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reality Smasher"); + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); setStopAt(1, PhaseStep.END_TURN); execute(); @@ -89,6 +101,8 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { @Test public void testCastingCreature() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); /* player.getPlayable does not take alternate @@ -100,7 +114,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Silvercoat Lion"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Silvercoat Lion"); - setChoice(playerA, true); + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -114,6 +128,8 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { @Test public void testCastingSplitCards() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); addCard(Zone.BATTLEFIELD, playerA, "Island", 1); @@ -123,7 +139,8 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Fire // Ice"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Fire", playerB); - setChoice(playerA, true); + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); + addTargetAmount(playerA, playerB, 2); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -139,6 +156,8 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { @Test public void testCastingShrapnelBlast() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); /* player.getPlayable does not take alternate @@ -151,7 +170,9 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Shrapnel Blast", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shrapnel Blast"); - setChoice(playerA, true); + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); + setChoice(playerA, "Ornithopter"); // sacrifice cost + addTarget(playerA, playerB); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -165,12 +186,14 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { /** * Spell get cast for 0 if Omniscience is being in play. But with - * Trinisphere it costs at least {3}. Cost/alternate cost (Omniscience) + + * Trinisphere it costs at least {3}. Cost/alternate cost (source: Omniscience) + * additional costs - cost reductions + minimum cost (Trinishpere) = total * cost. */ @Test public void testCastingWithTrinisphere() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); addCard(Zone.HAND, playerA, "Silvercoat Lion", 1); addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); @@ -181,7 +204,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Trinisphere", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Silvercoat Lion"); - setChoice(playerA, true); + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -218,7 +241,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Far // Away"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "fused Far // Away"); - setChoice(playerA, true); // Cast without paying its mana cost? + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); addTarget(playerA, "Silvercoat Lion"); addTarget(playerA, playerB); setChoice(playerB, "Pillarfield Ox"); @@ -243,6 +266,8 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { */ @Test public void testCastingWithFutureSight() { + setStrictChooseMode(true); + // You may cast nonland cards from your hand without paying their mana costs. addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); // Play with the top card of your library revealed. @@ -253,7 +278,6 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); skipInitShuffling(); - setStrictChooseMode(true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Silvercoat Lion"); setStopAt(1, PhaseStep.BEGIN_COMBAT); @@ -288,7 +312,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Bog Wraith", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); - setChoice(playerA, true); // cast without cost + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); setChoice(playerA, true); // pay Entwine addTarget(playerA, "Bog Wraith"); // target form mode 1 addTarget(playerA, playerB); // target for mode 2 @@ -329,7 +353,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { setStrictChooseMode(true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ancestral Vision", playerA); - setChoice(playerA, "Yes"); + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -365,12 +389,10 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Bog Wraith", 1); // Creature {3}{B} (3/3) castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Omniscience", true); - setChoice(playerA, true); // Pay alternative costs? ({W}{U}{B}{R}{G}) + setChoice(playerA, "Cast with alternative cost: {W}{U}{B}{R}{G} (source: Jodah, Archmage Eternal"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bog Wraith"); - // The order of the two alternate casting abilities is not fixed, so it's not clear which ability is asked for first - setChoice(playerA, false); // Pay alternative costs? ({W}{U}{B}{R}{G}) - setChoice(playerA, true); // Cast without paying its mana cost? + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -382,6 +404,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { @Test public void testJelevaCastingSavageBeatingFromExile() { + setStrictChooseMode(true); /* Jeleva, Nephalia's Scourge {1}{U}{B}{R} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/UseAlternateSourceCostsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/UseAlternateSourceCostsTest.java index b22c520e1e1..fed4d2b63bf 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/UseAlternateSourceCostsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/UseAlternateSourceCostsTest.java @@ -14,7 +14,7 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { @Test public void DreamHallsCastColoredSpell() { setStrictChooseMode(true); - + // Rather than pay the mana cost for a spell, its controller may discard a card that shares a color with that spell. addCard(Zone.BATTLEFIELD, playerA, "Dream Halls", 1); @@ -22,7 +22,7 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Lightning Bolt", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gray Ogre"); // Cast Orgre by discarding the Lightning Bolt - setChoice(playerA, true); // Pay alternative costs? (Discard a card that shares a color with that spell) + setChoice(playerA, "Cast with alternative cost: Discard a card that shares a color with that spell (source: Dream Halls"); setChoice(playerA, "Lightning Bolt"); setStopAt(1, PhaseStep.BEGIN_COMBAT); @@ -35,6 +35,8 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { @Test public void DreamHallsCantCastColorlessSpell() { + setStrictChooseMode(true); + // Rather than pay the mana cost for a spell, its controller may discard a card that shares a color with that spell. addCard(Zone.BATTLEFIELD, playerA, "Dream Halls", 1); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); // Add the mountains so the spell is included in teh available spells @@ -42,7 +44,7 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Juggernaut", 1); // Creature 5/3 - {4} addCard(Zone.HAND, playerA, "Haunted Plate Mail", 1); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Juggernaut"); // Cast Juggernaut by discarding Haunted Plate Mail may not work + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Juggernaut"); // Cast Juggernaut by discarding Haunted Plate Mail does not work setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -55,6 +57,8 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { @Test public void DreamHallsCastWithFutureSight() { + setStrictChooseMode(true); + // Rather than pay the mana cost for a spell, its controller may discard a card that shares a color with that spell. addCard(Zone.BATTLEFIELD, playerA, "Dream Halls", 1); // Play with the top card of your library revealed. @@ -67,6 +71,8 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { skipInitShuffling(); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gray Ogre"); // Cast Orgre by discarding the Lightning Bolt + setChoice(playerA, "Cast with alternative cost: Discard a card that shares a color with that spell (source: Dream Halls"); + setChoice(playerA, "Lightning Bolt"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -91,7 +97,7 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { checkPlayableAbility("can", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Abolish", true); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Abolish", "Alpha Myr"); - setChoice(playerA, true); // use alternative cost + setChoice(playerA, "Cast with alternative cost: Discard a Plains card (source: Abolish"); // use alternative cost setChoice(playerA, "Plains"); setStrictChooseMode(true); @@ -115,7 +121,7 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { checkPlayableAbility("can", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Abolish", true); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Abolish", "Alpha Myr"); - setChoice(playerA, true); // use alternative cost + setChoice(playerA, "Cast with alternative cost: Discard a Plains card (source: Abolish"); // use alternative cost setChoice(playerA, "Plains"); setStrictChooseMode(true); @@ -151,11 +157,11 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Invigorate"); // Instant {2}{G} addCard(Zone.BATTLEFIELD, playerA, "Forest"); addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Invigorate", "Silvercoat Lion"); - setChoice(playerA, true); // use alternative cost + setChoice(playerA, "Cast with alternative cost: An opponent gains 3 life (source: Invigorate"); // use alternative cost addTarget(playerA, playerB); // Opponent to gain live - + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -164,7 +170,7 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { assertPowerToughness(playerA, "Silvercoat Lion", 6, 6); assertLife(playerB, 23); } - + @Test public void test_Not_Playable_WithOpponentGainingLive() { // If you control a Forest, rather than pay Invigorate's mana cost, you may have an opponent gain 3 life. @@ -173,12 +179,12 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Forest"); addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); addCard(Zone.BATTLEFIELD, playerB, "Forest"); - - // can't see as playable because in graveyard + + // can't see as playable because in graveyard checkPlayableAbility("can't", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Invigorate", false); - + checkPlayableAbility("can't", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Cast Invigorate", false); - + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -187,7 +193,7 @@ public class UseAlternateSourceCostsTest extends CardTestPlayerBase { assertPowerToughness(playerA, "Silvercoat Lion", 2, 2); assertLife(playerB, 20); } - + @Test @Ignore // TODO: make test to check combo of alternative cost and cost reduction effects public void test_Playable_WithCostReduction() { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/DefenseGridTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/DefenseGridTest.java index dab35a9a864..3e7c5cce39b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/DefenseGridTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/DefenseGridTest.java @@ -7,7 +7,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ public class DefenseGridTest extends CardTestPlayerBase { @@ -15,10 +14,11 @@ public class DefenseGridTest extends CardTestPlayerBase { /** * Defense Grid vs Mindbreak Trap Not sure how this is coded, but Mindbreak * Trap should still cost 3 more (0+3=3). - * */ @Test public void testCostIncrease() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); addCard(Zone.HAND, playerA, "Lightning Bolt", 3); @@ -35,6 +35,7 @@ public class DefenseGridTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Mindbreak Trap", "Lightning Bolt^Lightning Bolt^Lightning Bolt"); + setChoice(playerB, "Cast with alternative cost: {0} (source: Mindbreak Trap"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/emblems/EmblemOfCardTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/emblems/EmblemOfCardTest.java index 924d6f13381..d38c1761b53 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/emblems/EmblemOfCardTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/emblems/EmblemOfCardTest.java @@ -64,6 +64,8 @@ public class EmblemOfCardTest extends CardTestPlayerBase { @Test public void testEmblemOfOmniscience() { + setStrictChooseMode(true); + // You may cast spells from your hand without paying their mana costs. addEmblem(playerA, new EmblemOfCard( CardRepository.instance.findCard("Omniscience", true).createMockCard() @@ -74,9 +76,11 @@ public class EmblemOfCardTest extends CardTestPlayerBase { // Trample addCard(Zone.HAND, playerA, "Colossal Dreadmaw"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Colossal Dreadmaw"); - waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); + assertPermanentCount(playerA, "Colossal Dreadmaw", 1); assertEmblemCount(playerA, 1); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/replacement/prevent/RefractionTrapTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/replacement/prevent/RefractionTrapTest.java index d59db4edafa..dabb366f313 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/replacement/prevent/RefractionTrapTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/replacement/prevent/RefractionTrapTest.java @@ -7,7 +7,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ @@ -19,6 +18,8 @@ public class RefractionTrapTest extends CardTestPlayerBase { */ @Test public void testPreventDamageFromSpell() { + setStrictChooseMode(true); + addCard(Zone.HAND, playerA, "Lightning Bolt"); addCard(Zone.BATTLEFIELD, playerA, "Mountain"); addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); @@ -30,10 +31,10 @@ public class RefractionTrapTest extends CardTestPlayerBase { // deals that much damage to any target. addCard(Zone.HAND, playerB, "Refraction Trap"); addCard(Zone.BATTLEFIELD, playerB, "Plains"); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Refraction Trap", "Silvercoat Lion", "Lightning Bolt"); - setChoice(playerB, true); + setChoice(playerB, "Cast with alternative cost: {W} (source: Refraction Trap"); setChoice(playerB, "Lightning Bolt"); setStopAt(1, PhaseStep.BEGIN_COMBAT); @@ -44,9 +45,9 @@ public class RefractionTrapTest extends CardTestPlayerBase { assertGraveyardCount(playerA, "Lightning Bolt", 1); assertGraveyardCount(playerB, "Refraction Trap", 1); - + assertGraveyardCount(playerA, "Silvercoat Lion", 1); - + } } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/cmm/DemonOfFatesDesignTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/cmm/DemonOfFatesDesignTest.java index b98e9239d71..7adbb3cbc80 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/cmm/DemonOfFatesDesignTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/cmm/DemonOfFatesDesignTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.single.cmm; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -28,7 +29,7 @@ public class DemonOfFatesDesignTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Glorious Anthem"); // Enchantment {1}{W}{W} castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Glorious Anthem"); - setChoice(playerA, true); // yes to alt cast + setChoice(playerA, "Cast with alternative cost: Pay 3 life"); // yes to alt cast setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -46,7 +47,7 @@ public class DemonOfFatesDesignTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Glorious Anthem"); // Enchantment {1}{W}{W} castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Glorious Anthem"); - setChoice(playerA, false); // no to alt cast + setChoice(playerA, TestPlayer.CHOICE_NORMAL_COST); // no to alt cast boolean hadError = false; try { @@ -70,7 +71,7 @@ public class DemonOfFatesDesignTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Absolute Law"); // Enchantment {1}{W} castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Glorious Anthem"); - setChoice(playerA, true); // yes to alt cast + setChoice(playerA, "Cast with alternative cost: Pay 3 life"); // yes to alt cast checkPlayableAbility("playable", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Absolute Law", false); @@ -124,7 +125,7 @@ public class DemonOfFatesDesignTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Absolute Law"); // Enchantment {1}{W} castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Glorious Anthem"); - setChoice(playerA, true); // yes to alt cast + setChoice(playerA, "Cast with alternative cost: Pay 3 life"); // yes to alt cast setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -133,7 +134,7 @@ public class DemonOfFatesDesignTest extends CardTestPlayerBase { assertLife(playerA, 20 - 3); castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Absolute Law"); - setChoice(playerA, true); // yes to alt cast + setChoice(playerA, "Cast with alternative cost: Pay 2 life"); // yes to alt cast setStopAt(3, PhaseStep.BEGIN_COMBAT); execute(); @@ -156,10 +157,10 @@ public class DemonOfFatesDesignTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Absolute Law"); // Enchantment {1}{W} castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Glorious Anthem", true); - setChoice(playerA, true); // yes to alt cast + setChoice(playerA, "Cast with alternative cost: Pay 3 life"); // yes to alt cast castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cloudshift", demon, true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Absolute Law"); - setChoice(playerA, true); // yes to alt cast + setChoice(playerA, "Cast with alternative cost: Pay 2 life"); // yes to alt cast setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -182,7 +183,7 @@ public class DemonOfFatesDesignTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Glorious Anthem"); // Enchantment {1}{W}{W} castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Glorious Anthem"); - setChoice(playerA, true); // yes to alt cast + setChoice(playerA, "Cast with alternative cost: Pay 3 life"); // yes to alt cast castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unsubstantiate", "Glorious Anthem"); // Did not keep the alt cost from the first cast @@ -199,16 +200,21 @@ public class DemonOfFatesDesignTest extends CardTestPlayerBase { public void DoubleDemonDoubleCast() { setStrictChooseMode(true); - addCard(Zone.BATTLEFIELD, playerA, demon, 2); + addCard(Zone.BATTLEFIELD, playerA, demon); + addCard(Zone.HAND, playerA, "Sakashima the Impostor"); // Clone with distinct name + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sakashima the Impostor", true); + setChoice(playerA, true); // yes to Sakashima to copy Demon + setChoice(playerA, demon); // copy Demon addCard(Zone.HAND, playerA, "Glorious Anthem"); // Enchantment {1}{W}{W} addCard(Zone.HAND, playerA, "Absolute Law"); // Enchantment {1}{W} castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Glorious Anthem", true); - setChoice(playerA, false); // no to the first one - setChoice(playerA, true); // yes to second one + setChoice(playerA, "Cast with alternative cost: Pay 3 life (source: Sakashima the Impostor"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Absolute Law"); - setChoice(playerA, true); // yes to first one + setChoice(playerA, "Cast with alternative cost: Pay 2 life (source: " + demon); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -218,39 +224,25 @@ public class DemonOfFatesDesignTest extends CardTestPlayerBase { assertLife(playerA, 20 - 3 - 2); } - //118.9a Only one alternative cost can be applied to any one spell as it’s being cast. - @Test - public void DoubleDemon() { - setStrictChooseMode(true); - - addCard(Zone.BATTLEFIELD, playerA, demon, 2); - - addCard(Zone.HAND, playerA, "Glorious Anthem"); // Enchantment {1}{W}{W} - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Glorious Anthem"); - setChoice(playerA, true); // yes to alt cast of first demon - // second demon has no choice to make. - - setStopAt(1, PhaseStep.BEGIN_COMBAT); - execute(); - - assertPermanentCount(playerA, "Glorious Anthem", 1); - assertLife(playerA, 20 - 3); - } - @Test public void DoubleDemonDoubleCast2() { setStrictChooseMode(true); - addCard(Zone.BATTLEFIELD, playerA, demon, 2); + addCard(Zone.BATTLEFIELD, playerA, demon); + addCard(Zone.HAND, playerA, "Sakashima the Impostor"); // Clone with distinct name + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sakashima the Impostor", true); + setChoice(playerA, true); // yes to Sakashima to copy Demon + setChoice(playerA, demon); // copy Demon addCard(Zone.HAND, playerA, "Glorious Anthem"); // Enchantment {1}{W}{W} addCard(Zone.HAND, playerA, "Absolute Law"); // Enchantment {1}{W} castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Glorious Anthem", true); - setChoice(playerA, true); // yes to the first one + setChoice(playerA, "Cast with alternative cost: Pay 3 life (source: " + demon); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Absolute Law"); - setChoice(playerA, true); // yes to second one + setChoice(playerA, "Cast with alternative cost: Pay 2 life (source: Sakashima the Impostor"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmc/PrimevalSpawnTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmc/PrimevalSpawnTest.java index bb1c0481e2e..642a68916bd 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmc/PrimevalSpawnTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmc/PrimevalSpawnTest.java @@ -76,7 +76,7 @@ public class PrimevalSpawnTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, omniscience); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, spawn); - setChoice(playerA, true); + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java index f7ebfae8cf3..e7e1c812faf 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/eld/OnceUponATimeTest.java @@ -6,7 +6,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ @@ -19,7 +18,7 @@ public class OnceUponATimeTest extends CardTestPlayerBase { addCard(Zone.LIBRARY, playerA, "Silvercoat Lion"); addCard(Zone.LIBRARY, playerA, "Plains", 4); skipInitShuffling(); - + // If this spell is the first spell you've cast this game, you may cast it without paying its mana cost. // Look at the top five cards of your library. // You may reveal a creature or land card from among them and put it into your hand. @@ -27,16 +26,16 @@ public class OnceUponATimeTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Once Upon a Time"); // Instant {1}{G} addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); addCard(Zone.HAND, playerA, "Forest", 1); - + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest"); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Once Upon a Time"); - - setChoice(playerA, false); // Cast without paying its mana cost? - + + setChoice(playerA, "Cast with no alternative cost: {1}{G}"); + setChoice(playerA, true); // Do you wish to reveal a creature or land card and put into your hand? addTarget(playerA, "Silvercoat Lion"); - + setStopAt(2, PhaseStep.END_TURN); execute(); @@ -44,7 +43,7 @@ public class OnceUponATimeTest extends CardTestPlayerBase { assertTappedCount("Forest", true, 2); assertHandCount(playerA, "Silvercoat Lion", 1); } - + @Test public void test_castForFree() { setStrictChooseMode(true); @@ -55,7 +54,7 @@ public class OnceUponATimeTest extends CardTestPlayerBase { addCard(Zone.LIBRARY, playerB, "Silvercoat Lion", 5); skipInitShuffling(); - + // If this spell is the first spell you've cast this game, you may cast it without paying its mana cost. // Look at the top five cards of your library. // You may reveal a creature or land card from among them and put it into your hand. @@ -64,15 +63,15 @@ public class OnceUponATimeTest extends CardTestPlayerBase { addCard(Zone.HAND, playerB, "Once Upon a Time"); // Instant {1}{G} castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Once Upon a Time"); - setChoice(playerA, true); // Cast without paying its mana cost? + setChoice(playerA, "Cast without paying its mana cost (source: Once Upon a Time"); setChoice(playerA, true); // Do you wish to reveal a creature or land card and put into your hand? addTarget(playerA, "Silvercoat Lion"); castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Once Upon a Time"); - setChoice(playerB, true); // Cast without paying its mana cost? + setChoice(playerB, "Cast without paying its mana cost (source: Once Upon a Time"); setChoice(playerB, true); // Do you wish to reveal a creature or land card and put into your hand? addTarget(playerB, "Silvercoat Lion"); - + setStopAt(2, PhaseStep.END_TURN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lrw/MulldrifterTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lrw/MulldrifterTest.java index c12d2a02312..f8590a7dc9a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/lrw/MulldrifterTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lrw/MulldrifterTest.java @@ -3,79 +3,84 @@ package org.mage.test.cards.single.lrw; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author escplan9 (Derek Monturo - dmontur1 at gmail dot com) */ public class MulldrifterTest extends CardTestPlayerBase { - + /** - * Reported bug: Muldrifter only draws 1 card. And only once during a turn if you haven't already drawn a card. - * Example, If it is my turn and I play Muldrifter, no card is drawn for trigger. - * - * If it is not my turn and I play Ghostly Flicker targeting Eternal Witness and Muldrifter, when Muldrifter enters play, only 1 card is drawn. - * + * Reported bug: Muldrifter only draws 1 card. And only once during a turn if you haven't already drawn a card. + * Example, If it is my turn and I play Muldrifter, no card is drawn for trigger. + *

+ * If it is not my turn and I play Ghostly Flicker targeting Eternal Witness and Muldrifter, when Muldrifter enters play, only 1 card is drawn. + *

* If I repeat the same thing in the same turn, the second time Muldrifter enters the battlefield, no cards are drawn. */ @Test public void testMulldrifterNotEvoked() { + setStrictChooseMode(true); // {4}{U} When Mulldrifter enters the battlefield, draw two cards. addCard(Zone.HAND, playerA, "Mulldrifter"); // 2/2 addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); addCard(Zone.BATTLEFIELD, playerA, "Island", 3); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mulldrifter"); - setChoice(playerA, false); // cast regularly, not evoked + setChoice(playerA, TestPlayer.CHOICE_NORMAL_COST); // cast regularly, not evoked setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); - + assertPermanentCount(playerA, "Mulldrifter", 1); assertHandCount(playerA, 2); // should have drawn 2 cards } - + /** * */ @Test public void testMulldrifterEvoked() { + setStrictChooseMode(true); // {4}{U} When Mulldrifter enters the battlefield, draw two cards. Evoke {2}{U} addCard(Zone.HAND, playerA, "Mulldrifter"); // 2/2 addCard(Zone.BATTLEFIELD, playerA, "Island", 3); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mulldrifter"); - setChoice(playerA, true); // only paid evoke cost + setChoice(playerA, "Cast with Evoke alternative cost: {2}{U} (source: Mulldrifter"); setStopAt(1, PhaseStep.BEGIN_COMBAT); + setChoice(playerA, "When this permanent enters the battlefield, if its evoke cost was paid, its controller sacrifices it"); // stack triggers + execute(); - + assertPermanentCount(playerA, "Mulldrifter", 0); assertGraveyardCount(playerA, "Mulldrifter", 1); assertHandCount(playerA, 2); // should have drawn 2 cards } - + /** * */ @Test public void testMulldrifterFlickered() { + setStrictChooseMode(true); // {4}{U} When Mulldrifter enters the battlefield, draw two cards. Evoke {2}{U} addCard(Zone.BATTLEFIELD, playerA, "Mulldrifter"); // 2/2 addCard(Zone.BATTLEFIELD, playerA, "Merfolk Looter"); // 1/1 addCard(Zone.BATTLEFIELD, playerA, "Island", 5); - + // Ghostly Flicker {2}{U} Instant // Exile two target artifacts, creatures, and/or lands you control, then return those cards to the battlefield under your control. addCard(Zone.HAND, playerA, "Ghostly Flicker"); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ghostly Flicker"); addTarget(playerA, "Mulldrifter^Merfolk Looter"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); - + assertPermanentCount(playerA, "Mulldrifter", 1); assertPermanentCount(playerA, "Merfolk Looter", 1); assertGraveyardCount(playerA, "Ghostly Flicker", 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/PrimalPrayersTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/PrimalPrayersTest.java new file mode 100644 index 00000000000..3142f9a7655 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/PrimalPrayersTest.java @@ -0,0 +1,169 @@ +package org.mage.test.cards.single.mh3; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.players.Player; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.player.TestPlayer; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class PrimalPrayersTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.p.PrimalPrayers Primal Prayers} {2}{G}{G} + * Enchantment + * When Primal Prayers enters the battlefield, you get {E}{E} (two energy counters). + * You may cast creature spells with mana value 3 or less by paying {E} rather than paying their mana costs. If you cast a spell this way, you may cast it as though it had flash. + */ + private static final String prayers = "Primal Prayers"; + + private static void checkEnergyCount(String message, Player player, int expected) { + Assert.assertEquals(message, expected, player.getCountersCount(CounterType.ENERGY)); + } + + @Test + public void test_DoesntGiveFlash_RegularCast() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, prayers); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.HAND, playerA, "Grizzly Bears"); + + checkPlayableAbility("1: regular cast at sorcery", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "Cast Grizzly Bears", true); + checkPlayableAbility("2: not able to use regular cast at wrong timing", 1, PhaseStep.BEGIN_COMBAT, playerA, + "Cast Grizzly Bears", false); + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + } + + @Test + public void test_GiveFlash_EnergyAlternativeCost() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, prayers); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, "Grizzly Bears"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, prayers, true); + checkPlayableAbility("1: alternative cast at sorcery", 1, PhaseStep.PRECOMBAT_MAIN, playerA, + "Cast Grizzly Bears", true); + checkPlayableAbility("2: able to use alternative cast at instant timing", 1, PhaseStep.BEGIN_COMBAT, playerA, + "Cast Grizzly Bears", true); + castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Grizzly Bears", true); + setChoice(playerA, "Cast with alternative cost: Pay {E}"); + + runCode("energy counter is 1", 1, PhaseStep.BEGIN_COMBAT, playerA, (info, player, game) -> checkEnergyCount(info, player, 1)); + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertPermanentCount(playerA, "Grizzly Bears", 1); + } + + @Test + public void test_UseEnergy() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, "Grizzly Bears", 3); + addCard(Zone.HAND, playerA, prayers); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, prayers, true); + runCode("1: energy counter is 2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> checkEnergyCount(info, player, 2)); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears", true); + setChoice(playerA, "Cast with alternative cost: Pay {E}"); // alternative cost chosen + runCode("2: energy counter is 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> checkEnergyCount(info, player, 1)); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears", true); + setChoice(playerA, "Cast with alternative cost: Pay {E}"); // alternative cost chosen + checkPlayableAbility("no more energy to cast third Bears", 1, PhaseStep.BEGIN_COMBAT, playerA, + "Cast Grizzly Bears", false); + runCode("3: energy counter is 0", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> checkEnergyCount(info, player, 0)); + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertPermanentCount(playerA, "Grizzly Bears", 2); + assertHandCount(playerA, "Grizzly Bears", 1); + } + + @Test + public void test_PayManaStill() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 6); + addCard(Zone.HAND, playerA, "Grizzly Bears"); + addCard(Zone.HAND, playerA, prayers); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, prayers, true); + runCode("1: energy counter is 2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> checkEnergyCount(info, player, 2)); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears", true); + setChoice(playerA, TestPlayer.CHOICE_NORMAL_COST); + runCode("2: energy counter is still 2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> checkEnergyCount(info, player, 2)); + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertTappedCount("Forest", true, 6); + assertPermanentCount(playerA, "Grizzly Bears", 1); + } + + @Test + public void test_CanNotCastWithoutEnergyAsFlash() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 6); + addCard(Zone.HAND, playerA, "Grizzly Bears", 1); + addCard(Zone.HAND, playerA, prayers); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, prayers, true); + castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Grizzly Bears"); + setChoice(playerA, TestPlayer.CHOICE_NORMAL_COST); // is not a valid choice + runCode("energy counter is 2", 1, PhaseStep.BEGIN_COMBAT, playerA, (info, player, game) -> checkEnergyCount(info, player, 2)); + + setStopAt(1, PhaseStep.END_COMBAT); + try { + execute(); + throw new IllegalStateException("Execute went without error"); + } catch (Throwable e) { + if (!e.getMessage().contains("Choose an alternative cost")) { + Assert.fail("Should have thrown error about missing the choice for the alternative cost, but got:\n" + e.getMessage()); + } + } + } + + @Test + public void test_CanNotCastWithOmniscienceAsFlash() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.HAND, playerA, prayers); + addCard(Zone.HAND, playerA, "Grizzly Bears", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, prayers, true); + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); // can choose Omniscience at sorcery speed. + runCode("energy counter is 2", 1, PhaseStep.BEGIN_COMBAT, playerA, (info, player, game) -> checkEnergyCount(info, player, 2)); + castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Grizzly Bears", true); + //setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); // can not choose Omniscience at instant speed. + + setStopAt(1, PhaseStep.END_COMBAT); + try { + execute(); + throw new IllegalStateException("Execute went without error"); + } catch (Throwable e) { + if (!e.getMessage().contains("Choose an alternative cost")) { + Assert.fail("Should have thrown error about missing the choice for the alternative cost, but got:\n" + e.getMessage()); + } + } + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/FreestriderCommandoTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/FreestriderCommandoTest.java index f1fe11f3735..31f37b593ed 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/FreestriderCommandoTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/FreestriderCommandoTest.java @@ -60,7 +60,7 @@ public class FreestriderCommandoTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, commando); castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, commando); - setChoice(playerA, true); // Omniscience asks for confirmation to cast to avoid missclick? + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); setStopAt(3, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/SatoruTheInfiltratorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/SatoruTheInfiltratorTest.java index 6e6644d8d5e..0173b4fc6c9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/SatoruTheInfiltratorTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/SatoruTheInfiltratorTest.java @@ -103,7 +103,7 @@ public class SatoruTheInfiltratorTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, commando); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando); - setChoice(playerA, true); // Omniscience asks for confirmation to cast to avoid missclick? + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -120,7 +120,7 @@ public class SatoruTheInfiltratorTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, satoru); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, satoru); - setChoice(playerA, true); // Omniscience asks for confirmation to cast to avoid missclick? + setChoice(playerA, "Cast without paying its mana cost (source: Omniscience"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/stx/BalefulMasteryTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/stx/BalefulMasteryTest.java index 31ea57ee995..b88af16fc5f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/stx/BalefulMasteryTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/stx/BalefulMasteryTest.java @@ -3,6 +3,7 @@ package org.mage.test.cards.single.stx; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; +import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** @@ -22,7 +23,7 @@ public class BalefulMasteryTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Witchbane Orb"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Goblin Piker"); - setChoice(playerA, false); // use normal cost + setChoice(playerA, TestPlayer.CHOICE_NORMAL_COST); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -45,7 +46,7 @@ public class BalefulMasteryTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Witchbane Orb"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Goblin Piker"); - setChoice(playerA, true); // use alternative cost + setChoice(playerA, "Cast with alternative cost: {1}{B}"); // use alternative cost addTarget(playerA, playerB); // select opponent setStrictChooseMode(true); @@ -70,12 +71,12 @@ public class BalefulMasteryTest extends CardTestPlayerBase { // cast 1 - alternative castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Goblin Piker"); - setChoice(playerA, true); // use alternative cost + setChoice(playerA, "Cast with alternative cost: {1}{B}"); // use alternative cost addTarget(playerA, playerB); // select opponent // cast 2 - normal castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Grizzly Bears"); - setChoice(playerA, false); // normal cast + setChoice(playerA, TestPlayer.CHOICE_NORMAL_COST); // normal cast setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -102,7 +103,7 @@ public class BalefulMasteryTest extends CardTestPlayerBase { // cast 1 - with alternative castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Goblin Piker"); - setChoice(playerA, true); // use alternative cost + setChoice(playerA, "Cast with alternative cost: {1}{B}"); // use alternative cost addTarget(playerA, playerB); // select opponent waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); checkGraveyardCount("after cast 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", 1); @@ -120,7 +121,7 @@ public class BalefulMasteryTest extends CardTestPlayerBase { // cast 2 - without alternative // possible bug: cost status can be found from previous object (e.g. it ask about opponent select, but must not) castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Grizzly Bears"); - setChoice(playerA, false); // do not use alternative cost + setChoice(playerA, TestPlayer.CHOICE_NORMAL_COST); // do not use alternative cost waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); checkGraveyardCount("after cast 2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", 1); checkHandCount("after cast 2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 0); @@ -151,7 +152,7 @@ public class BalefulMasteryTest extends CardTestPlayerBase { // cast with alternative activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 2); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Goblin Piker"); - setChoice(playerA, true); // use alternative cost + setChoice(playerA, "Cast with alternative cost: {1}{B}"); // use alternative cost // copy spell castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Twincast", "Cast Baleful Mastery", "Cast Baleful Mastery"); setChoice(playerA, true); // change target diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/wwk/MindbreakTrapTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/wwk/MindbreakTrapTest.java index 75cbcbbd7f0..24937481bbf 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/wwk/MindbreakTrapTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/wwk/MindbreakTrapTest.java @@ -27,6 +27,8 @@ public class MindbreakTrapTest extends CardTestPlayerBase { */ @Test public void mindBreakTrap_Exile_All_Spells() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 4); addCard(Zone.HAND, playerA, mindBreakTrap); @@ -37,9 +39,11 @@ public class MindbreakTrapTest extends CardTestPlayerBase { castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, shock, playerA); castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, grapeShot, playerA); + setChoice(playerB, false, 2); // do not change targets for copies waitStackResolved(2, PhaseStep.POSTCOMBAT_MAIN, 1); // Let the storm ability resolve to put the copies on the stack castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerA, mindBreakTrap, "Grapeshot^Grapeshot^Grapeshot"); + setChoice(playerA, "Cast with alternative cost: {0} (source: Mindbreak Trap"); setStopAt(2, PhaseStep.END_TURN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/ArchiveTrapTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/ArchiveTrapTest.java index c09fabacdfa..7a4d01f6f47 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/ArchiveTrapTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/ArchiveTrapTest.java @@ -37,7 +37,7 @@ public class ArchiveTrapTest extends CardTestPlayerBase { // must able to cast trap for {0} checkPlayableAbility("must able to cast", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Cast Archive Trap", true); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Archive Trap"); - setChoice(playerB, true); // use alternative cost + setChoice(playerB, "Cast with alternative cost: {0}"); // use alternative cost addTarget(playerB, playerA); setStrictChooseMode(true); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/CobraTrapTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/CobraTrapTest.java index 78977f97a23..638e2ac8a75 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/CobraTrapTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/CobraTrapTest.java @@ -22,6 +22,8 @@ public class CobraTrapTest extends CardTestPlayerBase { */ @Test public void testCard() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); addCard(Zone.HAND, playerA, "Cobra Trap"); addCard(Zone.BATTLEFIELD, playerB, "Mountain", 3); @@ -29,6 +31,7 @@ public class CobraTrapTest extends CardTestPlayerBase { castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Stone Rain", "Forest"); castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cobra Trap"); + setChoice(playerA, "Cast with alternative cost: {G}"); setStopAt(2, PhaseStep.END_TURN); execute(); @@ -44,6 +47,8 @@ public class CobraTrapTest extends CardTestPlayerBase { */ @Test public void testCardNegative() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); addCard(Zone.HAND, playerA, "Cobra Trap"); addCard(Zone.BATTLEFIELD, playerB, "Mountain", 3); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/InfernoTrapTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/InfernoTrapTest.java index f9a89ae4740..5b6df661668 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/InfernoTrapTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/zen/InfernoTrapTest.java @@ -10,7 +10,7 @@ import org.mage.test.serverside.base.CardTestPlayerBase; * {@link mage.cards.i.InfernoTrap Inferno Trap} * {3}{R} * Instant — Trap - * + *

* If you’ve been dealt damage by two or more creatures this turn, you may pay {R} rather than pay this spell’s mana cost. * Inferno Trap deals 4 damage to target creature. * @@ -20,6 +20,8 @@ public class InfernoTrapTest extends CardTestPlayerBase { @Test public void testTwoDamageStepsCountOnlyAsOneCreature() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); // Instant {3}{R} addCard(Zone.HAND, playerA, "Inferno Trap"); @@ -43,6 +45,8 @@ public class InfernoTrapTest extends CardTestPlayerBase { @Test public void testPlayByAlternateCost() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); // If you've been dealt damage by two or more creatures this turn, you may pay {R} rather than pay Inferno Trap's mana cost. // Inferno Trap deals 4 damage to target creature. @@ -56,6 +60,7 @@ public class InfernoTrapTest extends CardTestPlayerBase { attack(2, playerB, "Silvercoat Lion"); castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerA, "Inferno Trap", "Skyhunter Skirmisher"); + setChoice(playerA, "Cast with alternative cost: {R}"); // Use the alternative cost (regular cost can not be paid if chosen) setStopAt(2, PhaseStep.END_TURN); execute(); @@ -65,6 +70,7 @@ public class InfernoTrapTest extends CardTestPlayerBase { assertGraveyardCount(playerA, "Inferno Trap", 1); assertGraveyardCount(playerB, "Skyhunter Skirmisher", 1); + assertTapped("Mountain", true); } } 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 7b656cf461e..b90daabccfb 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 @@ -54,6 +54,7 @@ import mage.util.MultiAmountMessage; import mage.util.RandomUtil; import org.apache.log4j.Logger; import org.junit.Assert; +import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*; import java.io.Serializable; import java.util.*; @@ -61,8 +62,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*; - /** * Basic implementation of testable player * @@ -76,6 +75,7 @@ public class TestPlayer implements Player { public static final String TARGET_SKIP = "[target_skip]"; // stop/skip targeting public static final String CHOICE_SKIP = "[choice_skip]"; // stop/skip choice + public static final String CHOICE_NORMAL_COST = "Cast with no alternative cost: "; // when there is the possibility for an alternative cost, use the normal cost instead. public static final String MANA_CANCEL = "[mana_cancel]"; // cancel payment public static final String SKIP_FAILED_COMMAND = "[skip_failed_command]"; // skip next command in player's queue (can remove cast commands after try to activate) public static final String BLOCK_SKIP = "[block_skip]"; diff --git a/Mage/src/main/java/mage/MageIdentifier.java b/Mage/src/main/java/mage/MageIdentifier.java index fe8a52deb96..fe62619f236 100644 --- a/Mage/src/main/java/mage/MageIdentifier.java +++ b/Mage/src/main/java/mage/MageIdentifier.java @@ -69,7 +69,10 @@ public enum MageIdentifier { XandersPactAlternateCast, TheTombOfAclazotzWatcher, MeTheImmortalAlternateCast, - WithoutPayingManaCostAlternateCast; + WithoutPayingManaCostAlternateCast, + AlurenAlternateCast, + OfferingAlternateCast, + PrimalPrayersAlternateCast; /** * Additional text if there is need to differentiate two very similar effects diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index 059e089684f..ab458969a70 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -25,10 +25,7 @@ import mage.target.targetadjustment.TargetAdjuster; import mage.watchers.Watcher; import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; /** * Practically everything in the game is started from an Ability. This interface @@ -280,12 +277,17 @@ public interface Ability extends Controllable, Serializable { /** * Activates this ability prompting the controller to pay any mandatory * - * @param game A reference the {@link Game} for which this ability should be - * activated within. - * @param noMana Whether or not {@link ManaCosts} have to be paid. + * @param game A reference the {@link Game} for which this ability should be + * activated within. + * @param allowedIdentifiers Restrict alternative/regular cost depending (if contain MageIdentifier.Default, there is no restriction) + * @param noMana Whether or not {@link ManaCosts} have to be paid. * @return True if this ability was successfully activated. */ - boolean activate(Game game, boolean noMana); + boolean activate(Game game, Set allowedIdentifiers, boolean noMana); + + default boolean activate(Game game, boolean noMana) { + return activate(game, new HashSet<>(Arrays.asList(MageIdentifier.Default)), noMana); + } boolean isActivated(); @@ -472,7 +474,7 @@ public interface Ability extends Controllable, Serializable { */ String getGameLogMessage(Game game); - boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, boolean noMana, Player controller, Game game); + boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, Set allowedIdentifiers, boolean noMana, Player controller, Game game); /** * Finds the source object regardless of its zcc. Can be LKI from battlefield in some cases. diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index dd28682388b..00e2e6d2b9d 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -19,6 +19,9 @@ import mage.abilities.hint.Hint; import mage.abilities.icon.CardIcon; import mage.abilities.mana.ActivatedManaAbilityImpl; import mage.cards.Card; +import mage.choices.Choice; +import mage.choices.ChoiceHintType; +import mage.choices.ChoiceImpl; import mage.constants.*; import mage.game.Game; import mage.game.command.Dungeon; @@ -255,7 +258,7 @@ public abstract class AbilityImpl implements Ability { } @Override - public boolean activate(Game game, boolean noMana) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { Player controller = game.getPlayer(this.getControllerId()); if (controller == null) { return false; @@ -302,7 +305,9 @@ public abstract class AbilityImpl implements Ability { // or her intentions to pay any or all of those costs (see rule 601.2e). // A player can't apply two alternative methods of casting or two alternative costs to a single spell. if (isMainPartAbility) { - activateAlternateOrAdditionalCosts(sourceObject, noMana, controller, game); + if (!activateAlternateOrAdditionalCosts(sourceObject, allowedIdentifiers, noMana, controller, game)) { + return false; + } } // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost. An ability can @@ -451,8 +456,11 @@ public abstract class AbilityImpl implements Ability { return activated; } + /** + * @return true if choices for the activation were made (can be to activate with the regular cost) + */ @Override - public boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, boolean noMana, Player controller, Game game) { + public boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, Set allowedIdentifiers, boolean noMana, Player controller, Game game) { boolean canUseAlternativeCost = true; boolean canUseAdditionalCost = true; @@ -494,48 +502,104 @@ public abstract class AbilityImpl implements Ability { canUseAlternativeCost = false; } - boolean alternativeCostUsed = false; - if (sourceObject != null && !(sourceObject instanceof Permanent)) { - // it's important to apply alternative cost first - // example: Omniscience gives free mana as alternative, but Entwine ability adds {2} as additional - Abilities abilities = CardUtil.getAbilities(sourceObject, game); + // TODO: Why the check for permanent? + if (sourceObject == null || sourceObject instanceof Permanent) { + return true; + } - // 1. ALTERNATIVE COSTS - for (Ability ability : abilities) { - // if cast for noMana no Alternative costs are allowed - if (canUseAlternativeCost && !noMana && ability instanceof AlternativeSourceCosts) { - AlternativeSourceCosts alternativeSpellCosts = (AlternativeSourceCosts) ability; - if (alternativeSpellCosts.isAvailable(this, game)) { - if (alternativeSpellCosts.askToActivateAlternativeCosts(this, game)) { - // only one alternative costs may be activated - alternativeCostUsed = true; - break; - } - } - } - } - // controller specific alternate spell costs - if (canUseAlternativeCost && !noMana && !alternativeCostUsed) { - for (AlternativeSourceCosts alternativeSourceCosts : controller.getAlternativeSourceCosts()) { - if (alternativeSourceCosts.isAvailable(this, game)) { - if (alternativeSourceCosts.askToActivateAlternativeCosts(this, game)) { - // only one alternative costs may be activated - alternativeCostUsed = true; - break; - } - } - } - } + // it's important to apply alternative cost first + // example: Omniscience gives free mana as alternative, but Entwine ability adds {2} as additional + Abilities abilities = CardUtil.getAbilities(sourceObject, game); - // 2. ADDITIONAL COST - for (Ability ability : abilities) { - if (canUseAdditionalCost && ability instanceof OptionalAdditionalSourceCosts) { - ((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game); + // 1. ALTERNATIVE COSTS + // Collect all possible alternatives costs: + List possibleAlternatives = new ArrayList<>(); + for (Ability ability : abilities) { + // if cast for noMana no Alternative costs are allowed + if (canUseAlternativeCost && !noMana && ability instanceof AlternativeSourceCosts) { + AlternativeSourceCosts alternativeSpellCosts = (AlternativeSourceCosts) ability; + if (alternativeSpellCosts.isAvailable(this, game) + && alternativeSpellCosts.canActivateAlternativeCostsNow(this, game) + && (allowedIdentifiers.contains(MageIdentifier.Default) || allowedIdentifiers.contains(ability.getIdentifier()))) { + possibleAlternatives.add(alternativeSpellCosts); } } } - - return alternativeCostUsed; + // controller specific alternate spell costs + if (canUseAlternativeCost && !noMana) { + for (AlternativeSourceCosts alternativeSourceCosts : controller.getAlternativeSourceCosts()) { + if (alternativeSourceCosts.isAvailable(this, game) + && alternativeSourceCosts.canActivateAlternativeCostsNow(this, game) + && (allowedIdentifiers.contains(MageIdentifier.Default) || allowedIdentifiers.contains(alternativeSourceCosts.getIdentifier()))) { + possibleAlternatives.add(alternativeSourceCosts); + } + } + } + Player player = game.getPlayer(getControllerId()); + if (player == null) { + // No controller to activate. + return false; + } + Choice choice = new ChoiceImpl(false); // not required, cancelling will cancel the cast (as you could do once in the pay mana mode). + choice.setSubMessage("for casting " + CardUtil.getSourceLogName(game, "", this, "", "")); + AlternativeSourceCosts alternativeChosen = null; + if (!possibleAlternatives.isEmpty()) { + // At least one alternative cost is available. + // We open a menu for the player to choose up to one. + boolean mustChooseAlternative = !(allowedIdentifiers.contains(MageIdentifier.Default) || allowedIdentifiers.contains(getIdentifier())); + choice.setMessage( + mustChooseAlternative + ? "Choose an alternative cost" + : "You may choose an alternative cost" + ); + Map sort = new LinkedHashMap<>(); + int i; + for (i = 0; i < possibleAlternatives.size(); i++) { + String key = Integer.toString(i + 1); + sort.put(key, i); + AlternativeSourceCosts alternative = possibleAlternatives.get(i); + MageObject object = alternative.getSourceObject(game); + choice.withItem( + key, + possibleAlternatives.get(i).getAlternativeCostText(this, game), + i, + object != null ? ChoiceHintType.GAME_OBJECT : null, + object != null ? object.getId().toString() : null + ); + } + if (!mustChooseAlternative) { + // add the non-alternative cast as the last option. + String key = Integer.toString(i + 1); + sort.put(key, i); + choice.withItem( + key, + "Cast with no alternative cost: " + this.getManaCosts().getText(), + i, + ChoiceHintType.GAME_OBJECT, + sourceObject.getId().toString() + ); + } + if (!player.choose(Outcome.Benefit, choice, game)) { + return false; + } + String choiceKey = choice.getChoiceKey(); + if (sort.containsKey(choiceKey)) { + int choiceNumber = sort.get(choiceKey); + if (choiceNumber < possibleAlternatives.size()) { + alternativeChosen = possibleAlternatives.get(choiceNumber); + } + } + } + if (alternativeChosen != null) { + alternativeChosen.activateAlternativeCosts(this, game); + } + // 2. ADDITIONAL COST + for (Ability ability : abilities) { + if (canUseAdditionalCost && ability instanceof OptionalAdditionalSourceCosts) { + ((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game); + } + } + return true; } /** diff --git a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java index 05415628ecd..60c803102ee 100644 --- a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java @@ -1,6 +1,7 @@ package mage.abilities; import mage.ApprovingObject; +import mage.MageIdentifier; import mage.MageObject; import mage.abilities.condition.Condition; import mage.abilities.costs.Cost; @@ -215,8 +216,8 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa } @Override - public boolean activate(Game game, boolean noMana) { - if (!hasMoreActivationsThisTurn(game) || !super.activate(game, noMana)) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { + if (!hasMoreActivationsThisTurn(game) || !super.activate(game, allowedIdentifiers, noMana)) { return false; } ActivationInfo activationInfo = getActivationInfo(game); diff --git a/Mage/src/main/java/mage/abilities/SpellAbility.java b/Mage/src/main/java/mage/abilities/SpellAbility.java index ed9121f4c5d..2243a693b61 100644 --- a/Mage/src/main/java/mage/abilities/SpellAbility.java +++ b/Mage/src/main/java/mage/abilities/SpellAbility.java @@ -16,6 +16,7 @@ import mage.players.Player; import mage.util.CardUtil; import java.util.*; +import java.util.stream.Collectors; /** * @author BetaSteward_at_googlemail.com @@ -79,10 +80,20 @@ public class SpellAbility extends ActivatedAbilityImpl { * can be casted that are affected by the CastAsInstant effect. * (i.e. Vizier of the Menagerie and issue #5816) */ - public boolean spellCanBeActivatedRegularlyNow(UUID playerId, Game game) { + + private static final Set activationSetAllowAll = new HashSet(); + + static { + activationSetAllowAll.add(MageIdentifier.Default); + } + + /** + * @return the set of cast method MageIdentifer that are allowed to be cast at this time (if MageIdentifier.Default is in it, there is no restriction) + */ + public Set spellCanBeActivatedNow(UUID playerId, Game game) { MageObject object = game.getObject(sourceId); if (object == null) { - return false; + return Collections.emptySet(); } // forced to cast (can be part id or main id) @@ -93,23 +104,48 @@ public class SpellAbility extends ActivatedAbilityImpl { } for (UUID idToCheck : idsToCheck) { if (game.getState().getValue("PlayFromNotOwnHandZone" + idToCheck) != null) { - return (Boolean) game.getState().getValue("PlayFromNotOwnHandZone" + idToCheck); // card like Chandra, Torch of Defiance +1 loyal ability) + if ((Boolean) game.getState().getValue("PlayFromNotOwnHandZone" + idToCheck)) { + return activationSetAllowAll; + } else { + return Collections.emptySet(); + } } } - 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 + // OfferingAbility is doing side effects in its asThough computation. + // so we call it before the timing check. + // TODO: maybe Offering could be reworked with the MageIdentifier solution of linking + // CAST_AS_INSTANT with alternative cast methods? + Set asInstantApprovers = game.getContinuousEffects() + .asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game); + + if (timing == TimingRule.INSTANT || object.isInstant(game) || object.hasAbility(FlashAbility.getInstance(), game) - || game.canPlaySorcery(playerId); + || game.canPlaySorcery(playerId)) { + return activationSetAllowAll; + } + + // In case there is a need for as AsThoughEffectType.CAST_AS_INSTANT, we do collect the MageIdentifer of the approving objects + // to match later on with the real method to cast. When only non-Default MageIdentifier are used, only some of the alternative + // cast are possible to activate. + Set setOfIdentifier = new HashSet<>(); + setOfIdentifier.addAll( + asInstantApprovers + .stream() + .map(ApprovingObject::getApprovingAbility) + .map(Ability::getIdentifier) + .collect(Collectors.toSet()) + ); + return setOfIdentifier; } @Override public ActivationStatus canActivate(UUID playerId, Game game) { // spells can be cast from non hand zones, so must use custom check // no super.canActivate() call - - if (this.spellCanBeActivatedRegularlyNow(playerId, game)) { + Set allowedIdentifiers = this.spellCanBeActivatedNow(playerId, game); + if (!allowedIdentifiers.isEmpty()) { if (spellAbilityType == SpellAbilityType.SPLIT || spellAbilityType == SpellAbilityType.SPLIT_AFTERMATH) { return ActivationStatus.getFalse(); diff --git a/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceEachTurnAbility.java b/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceEachTurnAbility.java index 1a0189c3951..6ee178ca922 100644 --- a/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceEachTurnAbility.java +++ b/Mage/src/main/java/mage/abilities/common/CastFromGraveyardOnceEachTurnAbility.java @@ -1,7 +1,6 @@ package mage.abilities.common; import mage.MageIdentifier; -import mage.MageObject; import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.SpellAbility; @@ -96,8 +95,10 @@ class CastFromGraveyardOnceEffect extends AsThoughEffectImpl { if (spellAbility.getManaCosts().isEmpty()) { return false; } - return spellAbility.spellCanBeActivatedRegularlyNow(playerId, game) - && filter.match(cardToCheck, playerId, source, game); + Set allowedToBeCastNow = spellAbility.spellCanBeActivatedNow(playerId, game); + if (allowedToBeCastNow.contains(MageIdentifier.Default)) { + return filter.match(cardToCheck, playerId, source, game); + } } return false; } diff --git a/Mage/src/main/java/mage/abilities/common/SpellTransformedAbility.java b/Mage/src/main/java/mage/abilities/common/SpellTransformedAbility.java index 3ee7680deb2..5adbff755f5 100644 --- a/Mage/src/main/java/mage/abilities/common/SpellTransformedAbility.java +++ b/Mage/src/main/java/mage/abilities/common/SpellTransformedAbility.java @@ -1,5 +1,6 @@ package mage.abilities.common; +import mage.MageIdentifier; import mage.abilities.Ability; import mage.abilities.SpellAbility; import mage.abilities.costs.mana.ManaCostsImpl; @@ -10,6 +11,7 @@ import mage.constants.*; import mage.game.Game; import mage.game.stack.Spell; +import java.util.Set; import java.util.UUID; /** @@ -48,6 +50,7 @@ public class SpellTransformedAbility extends SpellAbility { this.setSpellAbilityCastMode(SpellAbilityCastMode.TRANSFORMED); //when casting this way, the card must have the TransformAbility from elsewhere } + protected SpellTransformedAbility(final SpellTransformedAbility ability) { super(ability); this.manaCost = ability.manaCost; @@ -59,8 +62,8 @@ public class SpellTransformedAbility extends SpellAbility { } @Override - public boolean activate(Game game, boolean noMana) { - if (super.activate(game, noMana)) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { + if (super.activate(game, allowedIdentifiers, noMana)) { game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getSourceId(), Boolean.TRUE); // TODO: must be removed after transform cards (one side) migrated to MDF engine (multiple sides) TransformedEffect effect = new TransformedEffect(); diff --git a/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java b/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java index 631ea69b8e4..62dd2fade0a 100644 --- a/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java +++ b/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java @@ -7,7 +7,6 @@ import mage.abilities.condition.Condition; import mage.abilities.costs.mana.ManaCost; import mage.cards.Card; import mage.constants.AbilityType; -import mage.constants.Outcome; import mage.constants.Zone; import mage.filter.FilterCard; import mage.game.Game; @@ -50,13 +49,13 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter } /** - * @param cost alternate cost to pay + * @param cost alternate cost to pay * @param condition only if the condition is true it's possible to use the - * alternate costs - * @param rule if != null used as rule text - * @param filter filters the cards this alternate cost can be applied to - * @param onlyMana if true only the mana costs are replaced by this costs, - * other costs stay untouched + * alternate costs + * @param rule if != null used as rule text + * @param filter filters the cards this alternate cost can be applied to + * @param onlyMana if true only the mana costs are replaced by this costs, + * other costs stay untouched */ public AlternativeCostSourceAbility(Cost cost, Condition condition, String rule, FilterCard filter, boolean onlyMana) { super(Zone.ALL, null); @@ -115,69 +114,88 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter } @Override - public boolean askToActivateAlternativeCosts(Ability ability, Game game) { - if (ability != null && AbilityType.SPELL == ability.getAbilityType()) { - if (filter != null) { - Card card = game.getCard(ability.getSourceId()); - if (!filter.match(card, ability.getControllerId(), ability, game)) { - return false; - } - } - Player player = game.getPlayer(ability.getControllerId()); - if (player != null) { - Costs alternativeCostsToCheck; - if (dynamicCost != null) { - alternativeCostsToCheck = new CostsImpl<>(); - alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game))); - } else { - alternativeCostsToCheck = this.alternateCosts; - } - - String costChoiceText; - if (dynamicCost != null) { - costChoiceText = dynamicCost.getText(ability, game); - } else { - costChoiceText = alternativeCostsToCheck.isEmpty() ? "Cast without paying its mana cost?" : "Pay alternative costs? (" + alternativeCostsToCheck.getText() + ')'; - } - if (alternativeCostsToCheck.canPay(ability, ability, ability.getControllerId(), game) - && player.chooseUse(Outcome.Benefit, costChoiceText, this, game)) { - if (ability instanceof SpellAbility) { - ability.getManaCostsToPay().removeIf(VariableCost.class::isInstance); - CardUtil.reduceCost((SpellAbility) ability, ability.getManaCosts()); - - } else { - ability.clearManaCostsToPay(); - } - if (!onlyMana) { - ability.clearCosts(); - } - for (AlternativeCost alternateCost : alternativeCostsToCheck) { - alternateCost.activate(); - for (Iterator it = ((Costs) alternateCost).iterator(); it.hasNext();) { - Cost costDetailed = (Cost) it.next(); - if (costDetailed instanceof ManaCost) { - ability.addManaCostsToPay((ManaCost) costDetailed.copy()); - } else if (costDetailed != null) { - ability.addCost(costDetailed.copy()); - } - } - } - - // Those cost have been paid, we want to store them. - if (dynamicCost != null) { - rememberDynamicCost(game, ability, alternativeCostsToCheck); - } - - // save activated status - doActivate(game, ability); - } else { - return false; - } - } else { + public boolean canActivateAlternativeCostsNow(Ability ability, Game game) { + if (ability == null || !AbilityType.SPELL.equals(ability.getAbilityType())) { + return isActivated(ability, game); + } + if (filter != null) { + Card card = game.getCard(ability.getSourceId()); + if (!filter.match(card, ability.getControllerId(), ability, game)) { return false; } } - return isActivated(ability, game); + Player player = game.getPlayer(ability.getControllerId()); + if (player == null) { + return false; + } + Costs alternativeCostsToCheck; + if (dynamicCost != null) { + alternativeCostsToCheck = new CostsImpl<>(); + alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game))); + } else { + alternativeCostsToCheck = this.alternateCosts; + } + + return alternativeCostsToCheck.canPay(ability, ability, ability.getControllerId(), game); + } + + @Override + public String getAlternativeCostText(Ability ability, Game game) { + if (dynamicCost != null) { + return "Cast with alternative cost: " + dynamicCost.getText(ability, game) + CardUtil.getSourceLogName(game, this); + } else { + Costs alternativeCostsToCheck; + if (dynamicCost != null) { + alternativeCostsToCheck = new CostsImpl<>(); + alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game))); + } else { + alternativeCostsToCheck = this.alternateCosts; + } + return alternativeCostsToCheck.isEmpty() + ? "Cast without paying its mana cost" + CardUtil.getSourceLogName(game, this) + : "Cast with alternative cost: " + alternativeCostsToCheck.getText() + CardUtil.getSourceLogName(game, this); + } + } + + @Override + public boolean activateAlternativeCosts(Ability ability, Game game) { + Costs alternativeCostsToCheck; + if (dynamicCost != null) { + alternativeCostsToCheck = new CostsImpl<>(); + alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game))); + } else { + alternativeCostsToCheck = this.alternateCosts; + } + if (ability instanceof SpellAbility) { + ability.getManaCostsToPay().removeIf(VariableCost.class::isInstance); + CardUtil.reduceCost((SpellAbility) ability, ability.getManaCosts()); + + } else { + ability.clearManaCostsToPay(); + } + if (!onlyMana) { + ability.clearCosts(); + } + for (AlternativeCost alternateCost : alternativeCostsToCheck) { + alternateCost.activate(); + for (Iterator it = ((Costs) alternateCost).iterator(); it.hasNext(); ) { + Cost costDetailed = (Cost) it.next(); + if (costDetailed instanceof ManaCost) { + ability.addManaCostsToPay((ManaCost) costDetailed.copy()); + } else if (costDetailed != null) { + ability.addCost(costDetailed.copy()); + } + } + } + + // Those cost have been paid, we want to store them. + if (dynamicCost != null) { + rememberDynamicCost(game, ability, alternativeCostsToCheck); + } + + // save activated status + doActivate(game, ability); + return true; } protected void doActivate(Game game, Ability ability) { @@ -217,8 +235,8 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter * @param game * @param source * @param alternativeCostOriginalId you must save originalId on card's - * creation - * @param searchPrevZCC true on battlefield, false on stack + * creation + * @param searchPrevZCC true on battlefield, false on stack * @return */ public static boolean getActivatedStatus(Game game, Ability source, UUID alternativeCostOriginalId, boolean searchPrevZCC) { @@ -280,8 +298,8 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter sb.append(CardUtil.concatWithAnd(alternateCosts .stream() .map(cost -> cost.getCost() instanceof ManaCost - ? "pay " + cost.getText(true) - : cost.getText(true)) + ? "pay " + cost.getText(true) + : cost.getText(true)) .map(CardUtil::getTextWithFirstCharLowerCase) .collect(Collectors.toList()))); if (condition == null || alternateCosts.size() == 1) { diff --git a/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCosts.java b/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCosts.java index 14a4737aec7..f4c8f050698 100644 --- a/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCosts.java +++ b/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCosts.java @@ -1,5 +1,6 @@ package mage.abilities.costs; +import mage.MageIdentifier; import mage.abilities.Ability; import mage.game.Game; @@ -10,16 +11,34 @@ import mage.game.Game; * * @author LevelX2 */ -public interface AlternativeSourceCosts { +public interface AlternativeSourceCosts extends Ability { /** - * Ask the player if they want to use the alternative costs + * If non-Default, allow to link this cost to permission abilities + */ + MageIdentifier getIdentifier(); + + /** + * Check that the alternative ability can be used for the ability. * * @param ability ability the alternative cost is activated for * @param game * @return */ - boolean askToActivateAlternativeCosts(Ability ability, Game game); + boolean canActivateAlternativeCostsNow(Ability ability, Game game); + + String getAlternativeCostText(Ability ability, Game game); + + /** + * activate a specific alternative cost. + * A check to canActivateAlternativeCostsNow should be made before-end + * to check if it is valid. + * + * @param ability ability the alternative cost is activated for + * @param game + * @return + */ + boolean activateAlternativeCosts(Ability ability, Game game); /** * Is the alternative spell cost currently available diff --git a/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCostsImpl.java index 13c06b8720e..0f947e5568e 100644 --- a/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/AlternativeSourceCostsImpl.java @@ -1,11 +1,10 @@ package mage.abilities.costs; import mage.abilities.Ability; -import mage.abilities.SpellAbility; import mage.abilities.StaticAbility; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.constants.Outcome; +import mage.constants.AbilityType; import mage.constants.Zone; import mage.game.Game; import mage.players.Player; @@ -21,8 +20,9 @@ public abstract class AlternativeSourceCostsImpl extends StaticAbility implement protected final AlternativeCost alternativeCost; protected final String reminderText; protected final String activationKey; - protected static String getActivationKey(String name){ - return name+"ActivationKey"; + + protected static String getActivationKey(String name) { + return name + "ActivationKey"; } protected AlternativeSourceCostsImpl(String name, String reminderText, String manaString) { @@ -44,24 +44,24 @@ public abstract class AlternativeSourceCostsImpl extends StaticAbility implement this.activationKey = ability.activationKey; } + @Override - public boolean askToActivateAlternativeCosts(Ability ability, Game game) { - if (ability instanceof SpellAbility) { - handleActivatingAlternativeCosts(ability, game); + public boolean canActivateAlternativeCostsNow(Ability ability, Game game) { + if (ability == null || !AbilityType.SPELL.equals(ability.getAbilityType())) { + return isActivated(ability, game); } - return isActivated(ability, game); + Player player = game.getPlayer(ability.getControllerId()); + return player != null && alternativeCost.canPay(ability, this, player.getId(), game); } - protected boolean handleActivatingAlternativeCosts(Ability ability, Game game) { - Player player = game.getPlayer(ability.getControllerId()); - if (player == null) { - return false; - } + @Override + public String getAlternativeCostText(Ability ability, Game game) { + return "Cast with " + this.name + " alternative cost: " + alternativeCost.getText(true) + CardUtil.getSourceLogName(game, this); + } + + @Override + public boolean activateAlternativeCosts(Ability ability, Game game) { this.resetCost(); - if (!alternativeCost.canPay(ability, this, player.getId(), game) - || !player.chooseUse(Outcome.Benefit, "Cast this for its " + this.name + " cost? (" + alternativeCost.getText(true) + ')', ability, game)) { - return false; - } ability.setCostsTag(activationKey, null); alternativeCost.activate(); diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/CastFromHandWithoutPayingManaCostEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/CastFromHandWithoutPayingManaCostEffect.java index 3370b3143b7..0a561f533a4 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/CastFromHandWithoutPayingManaCostEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/CastFromHandWithoutPayingManaCostEffect.java @@ -16,8 +16,6 @@ import mage.filter.StaticFilters; import mage.game.Game; import mage.players.Player; -import java.util.UUID; - public class CastFromHandWithoutPayingManaCostEffect extends ContinuousEffectImpl { private final AlternativeCostSourceAbility alternativeCastingCostAbility; @@ -54,18 +52,13 @@ public class CastFromHandWithoutPayingManaCostEffect extends ContinuousEffectImp return new CastFromHandWithoutPayingManaCostEffect(this); } - @Override - public void init(Ability source, Game game, UUID activePlayerId) { - super.init(source, game, activePlayerId); - alternativeCastingCostAbility.setSourceId(source.getSourceId()); - } - @Override public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { Player controller = game.getPlayer(source.getControllerId()); if (controller == null) { return false; } + alternativeCastingCostAbility.setSourceId(source.getSourceId()); controller.getAlternativeSourceCosts().add(alternativeCastingCostAbility); return true; } diff --git a/Mage/src/main/java/mage/abilities/keyword/BlitzAbility.java b/Mage/src/main/java/mage/abilities/keyword/BlitzAbility.java index 9453b049fd8..fce4d088c8f 100644 --- a/Mage/src/main/java/mage/abilities/keyword/BlitzAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/BlitzAbility.java @@ -1,5 +1,6 @@ package mage.abilities.keyword; +import mage.MageIdentifier; import mage.abilities.Ability; import mage.abilities.SpellAbility; import mage.abilities.common.DiesSourceTriggeredAbility; @@ -19,6 +20,8 @@ import mage.constants.TimingRule; import mage.game.Game; import mage.target.targetpointer.FixedTarget; +import java.util.Set; + /** * @author TheElk801 */ @@ -73,8 +76,8 @@ public class BlitzAbility extends SpellAbility { } @Override - public boolean activate(Game game, boolean noMana) { - if (!super.activate(game, noMana)) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { + if (!super.activate(game, allowedIdentifiers, noMana)) { return false; } this.setCostsTag(BLITZ_ACTIVATION_VALUE_KEY, null); diff --git a/Mage/src/main/java/mage/abilities/keyword/EmergeAbility.java b/Mage/src/main/java/mage/abilities/keyword/EmergeAbility.java index 47578e51a3e..c1678cd031c 100644 --- a/Mage/src/main/java/mage/abilities/keyword/EmergeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/EmergeAbility.java @@ -1,6 +1,7 @@ package mage.abilities.keyword; import mage.ApprovingObject; +import mage.MageIdentifier; import mage.MageObjectReference; import mage.Mana; import mage.abilities.SpellAbility; @@ -20,6 +21,7 @@ import mage.players.Player; import mage.target.common.TargetSacrifice; import mage.util.CardUtil; +import java.util.Set; import java.util.UUID; /** @@ -101,7 +103,7 @@ public class EmergeAbility extends SpellAbility { } @Override - public boolean activate(Game game, boolean noMana) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { Player controller = game.getPlayer(this.getControllerId()); if (controller != null) { TargetSacrifice target = new TargetSacrifice(filter); @@ -110,7 +112,7 @@ public class EmergeAbility extends SpellAbility { Permanent creature = game.getPermanent(target.getFirstTarget()); if (creature != null) { CardUtil.reduceCost(this, creature.getManaValue()); - if (super.activate(game, false)) { + if (super.activate(game, allowedIdentifiers, false)) { MageObjectReference mor = new MageObjectReference(creature, game); if (creature.sacrifice(this, game)) { this.setCostsTag(EMERGE_ACTIVATION_CREATURE_REFERENCE, mor); //Can access with LKI afterwards diff --git a/Mage/src/main/java/mage/abilities/keyword/EscapeAbility.java b/Mage/src/main/java/mage/abilities/keyword/EscapeAbility.java index 840779b1936..aa370c2649d 100644 --- a/Mage/src/main/java/mage/abilities/keyword/EscapeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/EscapeAbility.java @@ -1,5 +1,6 @@ package mage.abilities.keyword; +import mage.MageIdentifier; import mage.abilities.SpellAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.Costs; @@ -15,6 +16,7 @@ import mage.game.Game; import mage.target.common.TargetCardInYourGraveyard; import mage.util.CardUtil; +import java.util.Set; import java.util.UUID; /** @@ -97,8 +99,8 @@ public class EscapeAbility extends SpellAbility { } @Override - public boolean activate(Game game, boolean noMana) { - if (super.activate(game, noMana)) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { + if (super.activate(game, allowedIdentifiers, noMana)) { game.getState().setValue(CASTED_WITH_ESCAPE_KEY + getSourceId().toString() + (getSourceObjectZoneChangeCounter() + 1), Boolean.TRUE); return true; } diff --git a/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java b/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java index 17c667914e0..c91a6f2cbbd 100644 --- a/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java @@ -1,5 +1,6 @@ package mage.abilities.keyword; +import mage.MageIdentifier; import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.SpecialAction; @@ -17,6 +18,7 @@ import mage.players.Player; import mage.util.CardUtil; import java.util.List; +import java.util.Set; import java.util.UUID; /** @@ -78,7 +80,8 @@ public class PlotAbility extends SpecialAction { // Not Allowed from other zones return ActivationStatus.getFalse(); } - if (!card.getSpellAbility().spellCanBeActivatedRegularlyNow(playerId, game)) { + Set allowedToBeCastNow = card.getSpellAbility().spellCanBeActivatedNow(playerId, game); + if (!allowedToBeCastNow.contains(MageIdentifier.Default) && !allowedToBeCastNow.contains(card.getSpellAbility().getIdentifier())) { return ActivationStatus.getFalse(); } return super.canActivate(playerId, game); diff --git a/Mage/src/main/java/mage/abilities/keyword/SpectacleAbility.java b/Mage/src/main/java/mage/abilities/keyword/SpectacleAbility.java index 0216e6acfb9..9af6bd5f940 100644 --- a/Mage/src/main/java/mage/abilities/keyword/SpectacleAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/SpectacleAbility.java @@ -1,6 +1,7 @@ package mage.abilities.keyword; import mage.ApprovingObject; +import mage.MageIdentifier; import mage.abilities.SpellAbility; import mage.abilities.costs.mana.ManaCost; import mage.abilities.dynamicvalue.common.OpponentsLostLifeCount; @@ -10,6 +11,7 @@ import mage.constants.SpellAbilityType; import mage.constants.Zone; import mage.game.Game; +import java.util.Set; import java.util.UUID; /** @@ -53,9 +55,9 @@ public class SpectacleAbility extends SpellAbility { } @Override - public boolean activate(Game game, boolean noMana) { - if (super.activate(game, noMana)) { - this.setCostsTag(SPECTACLE_ACTIVATION_VALUE_KEY,null); + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { + if (super.activate(game, allowedIdentifiers, noMana)) { + this.setCostsTag(SPECTACLE_ACTIVATION_VALUE_KEY, null); return true; } return false; diff --git a/Mage/src/main/java/mage/abilities/keyword/SurgeAbility.java b/Mage/src/main/java/mage/abilities/keyword/SurgeAbility.java index 3a677762d67..f440c3adc37 100644 --- a/Mage/src/main/java/mage/abilities/keyword/SurgeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/SurgeAbility.java @@ -1,6 +1,7 @@ package mage.abilities.keyword; import mage.ApprovingObject; +import mage.MageIdentifier; import mage.abilities.SpellAbility; import mage.abilities.costs.mana.ManaCostsImpl; import mage.cards.Card; @@ -10,6 +11,7 @@ import mage.game.Game; import mage.players.Player; import mage.watchers.common.CastSpellLastTurnWatcher; +import java.util.Set; import java.util.UUID; /** @@ -63,8 +65,8 @@ public class SurgeAbility extends SpellAbility { } @Override - public boolean activate(Game game, boolean noMana) { - if (super.activate(game, noMana)) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { + if (super.activate(game, allowedIdentifiers, noMana)) { this.setCostsTag(SURGE_ACTIVATION_VALUE_KEY, null); return true; } diff --git a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java index 64f4d4843ef..e650da64961 100644 --- a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java @@ -1,6 +1,7 @@ package mage.abilities.keyword; import mage.ApprovingObject; +import mage.MageIdentifier; import mage.abilities.Ability; import mage.abilities.SpecialAction; import mage.abilities.TriggeredAbilityImpl; @@ -27,6 +28,7 @@ import mage.util.CardUtil; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.UUID; /** @@ -216,7 +218,8 @@ public class SuspendAbility extends SpecialAction { if (card == null) { return ActivationStatus.getFalse(); } - if (!card.getSpellAbility().spellCanBeActivatedRegularlyNow(playerId, game)) { + Set allowedToBeCastNow = card.getSpellAbility().spellCanBeActivatedNow(playerId, game); + if (!allowedToBeCastNow.contains(MageIdentifier.Default) && !allowedToBeCastNow.contains(card.getSpellAbility().getIdentifier())) { return ActivationStatus.getFalse(); } return super.canActivate(playerId, game); diff --git a/Mage/src/main/java/mage/abilities/mana/ActivateIfConditionManaAbility.java b/Mage/src/main/java/mage/abilities/mana/ActivateIfConditionManaAbility.java index abf4e1e38af..c3c9c68042d 100644 --- a/Mage/src/main/java/mage/abilities/mana/ActivateIfConditionManaAbility.java +++ b/Mage/src/main/java/mage/abilities/mana/ActivateIfConditionManaAbility.java @@ -6,7 +6,6 @@ import mage.abilities.costs.Cost; import mage.abilities.effects.mana.AddConditionalColorlessManaEffect; import mage.abilities.effects.mana.BasicManaEffect; import mage.constants.Zone; -import mage.game.Game; public class ActivateIfConditionManaAbility extends ActivatedManaAbilityImpl { @@ -26,11 +25,6 @@ public class ActivateIfConditionManaAbility extends ActivatedManaAbilityImpl { super(ability); } - @Override - public boolean activate(Game game, boolean noMana) { - return super.activate(game, noMana); - } - @Override public String getRule() { return super.getRule() + " Activate only if " + condition.toString() + '.'; diff --git a/Mage/src/main/java/mage/abilities/mana/LimitedTimesPerTurnActivatedManaAbility.java b/Mage/src/main/java/mage/abilities/mana/LimitedTimesPerTurnActivatedManaAbility.java index 54c1058d629..84f3c753328 100644 --- a/Mage/src/main/java/mage/abilities/mana/LimitedTimesPerTurnActivatedManaAbility.java +++ b/Mage/src/main/java/mage/abilities/mana/LimitedTimesPerTurnActivatedManaAbility.java @@ -1,6 +1,7 @@ package mage.abilities.mana; +import mage.MageIdentifier; import mage.Mana; import mage.abilities.costs.Cost; import mage.abilities.effects.mana.AddManaOfAnyColorEffect; @@ -12,6 +13,7 @@ import mage.util.CardUtil; import java.util.Arrays; import java.util.List; +import java.util.Set; /** * @author LevelX2, Susucr @@ -50,9 +52,9 @@ public class LimitedTimesPerTurnActivatedManaAbility extends ActivatedManaAbilit } @Override - public boolean activate(Game game, boolean noMana) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { if (canActivate(this.controllerId, game).canActivate()) { - return super.activate(game, noMana); + return super.activate(game, allowedIdentifiers, noMana); } return false; } diff --git a/Mage/src/main/java/mage/choices/ChoiceImpl.java b/Mage/src/main/java/mage/choices/ChoiceImpl.java index af9c8f60231..72cf891ed38 100644 --- a/Mage/src/main/java/mage/choices/ChoiceImpl.java +++ b/Mage/src/main/java/mage/choices/ChoiceImpl.java @@ -311,7 +311,10 @@ public class ChoiceImpl implements Choice { // no key answer found, try to match by text starting with for (String needChoice : answers) { for (Map.Entry currentChoice : this.getKeyChoices().entrySet()) { - if (currentChoice.getValue().startsWith(needChoice)) { + String choiceValue = currentChoice.getValue(); + // Clean any html part (for easier unit test matching) + String cleanedChoiceValue = choiceValue.replaceAll("<[^<>]*>", ""); + if (choiceValue.startsWith(needChoice) || cleanedChoiceValue.startsWith(needChoice)) { if (removeSelectAnswerFromList) { this.setChoiceByKey(currentChoice.getKey(), false); answers.remove(needChoice); @@ -324,7 +327,9 @@ public class ChoiceImpl implements Choice { // string mode for (String needChoice : answers) { for (String currentChoice : this.getChoices()) { - if (currentChoice.equals(needChoice)) { + // Clean any html part (for easier unit test matching) + String cleanedChoiceValue = currentChoice.replaceAll("<[^<>]*>", ""); + if (currentChoice.equals(needChoice) || cleanedChoiceValue.equals(needChoice)) { if (removeSelectAnswerFromList) { this.setChoice(needChoice, false); answers.remove(needChoice); diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 56f7c5cc3fc..5c2b69d19e1 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -161,10 +161,10 @@ public class Spell extends StackObjectImpl implements Card { this.startingDefense = spell.startingDefense; } - public boolean activate(Game game, boolean noMana) { + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { setCurrentActivatingManaAbilitiesStep(ActivationManaAbilityStep.BEFORE); // mana payment step started, can use any mana abilities, see AlternateManaPaymentAbility - if (!ability.activate(game, noMana)) { + if (!ability.activate(game, allowedIdentifiers, noMana)) { return false; } @@ -182,7 +182,7 @@ public class Spell extends StackObjectImpl implements Card { // see https://github.com/magefree/mage/issues/6603 payNoMana |= ability.getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED; - if (!spellAbility.activate(game, payNoMana)) { + if (!spellAbility.activate(game, allowedIdentifiers, payNoMana)) { return false; } } diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index dedc8115a29..d87f9fdf871 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -388,8 +388,8 @@ public class StackAbility extends StackObjectImpl implements Ability { } @Override - public boolean activate(Game game, boolean noMana) { - return ability.activate(game, noMana); + public boolean activate(Game game, Set allowedIdentifiers, boolean noMana) { + return ability.activate(game, allowedIdentifiers, noMana); } @Override @@ -584,7 +584,7 @@ public class StackAbility extends StackObjectImpl implements Ability { } @Override - public boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, boolean noMana, Player controller, Game game) { + public boolean activateAlternateOrAdditionalCosts(MageObject sourceObject, Set allowedIdentifiers, boolean noMana, Player controller, Game game) { throw new UnsupportedOperationException("Not supported yet."); } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index b65611f6aaf..ebe719cbbf8 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1231,6 +1231,7 @@ public abstract class PlayerImpl implements Player, Serializable { // Use ability copy to avoid problems with targets and costs on recast (issue https://github.com/magefree/mage/issues/5189). SpellAbility ability = originalAbility.copy(); + Set allowedIdentifiers = originalAbility.spellCanBeActivatedNow(getId(), game); ability.setControllerId(getId()); ability.setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(ability.getSourceId())); @@ -1265,7 +1266,6 @@ public abstract class PlayerImpl implements Player, Serializable { 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; @@ -1291,7 +1291,7 @@ public abstract class PlayerImpl implements Player, Serializable { spell.getSpellAbility().getId(), spell.getSpellAbility(), playerId, approvingObject); castEvent.setZone(fromZone); game.fireEvent(castEvent); - if (spell.activate(game, noMana)) { + if (spell.activate(game, allowedIdentifiers, noMana)) { GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST, ability.getId(), ability, playerId, approvingObject); castedEvent.setZone(fromZone); @@ -1673,7 +1673,7 @@ public abstract class PlayerImpl implements Player, Serializable { * @return */ public static Map getCastableSpellAbilities(Game game, UUID playerId, MageObject object, Zone zone, boolean noMana) { - // it uses simple check from spellCanBeActivatedRegularlyNow + // it uses simple check from spellCanBeActivatedNow // reason: no approved info here (e.g. forced to choose spell ability from cast card) LinkedHashMap useable = new LinkedHashMap<>(); Abilities allAbilities; @@ -1695,7 +1695,8 @@ public abstract class PlayerImpl implements Player, Serializable { // If the card has any mandatory additional costs, those must be paid to cast the spell. // (2021-02-05) if (!noMana) { - if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + Set allowedToBeCastNow = spellAbility.spellCanBeActivatedNow(playerId, game); + if (allowedToBeCastNow.contains(MageIdentifier.Default) || allowedToBeCastNow.contains(spellAbility.getIdentifier())) { useable.put(spellAbility.getId(), spellAbility); // example: Chandra, Torch of Defiance +1 loyal ability } return useable; @@ -1735,10 +1736,12 @@ public abstract class PlayerImpl implements Player, Serializable { } } return useable; - default: - if (spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)) { + default: { + Set allowedToBeCastNow = spellAbility.spellCanBeActivatedNow(playerId, game); + if (allowedToBeCastNow.contains(MageIdentifier.Default) || allowedToBeCastNow.contains(spellAbility.getIdentifier())) { useable.put(spellAbility.getId(), spellAbility); } + } } } return useable; @@ -3622,19 +3625,29 @@ public abstract class PlayerImpl implements Player, Serializable { game.getContinuousEffects().costModification(copy, game); } boolean canBeCastRegularly = true; - if (copy instanceof SpellAbility && copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) { - // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost... - // 117.6a (...) If an alternative cost is applied to an unpayable cost, - // including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid. - canBeCastRegularly = false; + Set allowedIdentifiers = null; + if (copy instanceof SpellAbility) { + if (copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) { + // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost... + // 117.6a (...) If an alternative cost is applied to an unpayable cost, + // including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid. + canBeCastRegularly = false; + } + allowedIdentifiers = ((SpellAbility) copy).spellCanBeActivatedNow(playerId, game); + if (!allowedIdentifiers.contains(MageIdentifier.Default)) { + // If the timing restriction is lifted only for specific MageIdentifier, the default cast can not be used. + canBeCastRegularly = false; + } } if (canBeCastRegularly && canPayMinimumManaCost(copy, availableMana, game)) { return true; } // ALTERNATIVE COST FROM dynamic effects - for (MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) { + if (allowedIdentifiers != null && !(allowedIdentifiers.contains(MageIdentifier.Default) || allowedIdentifiers.contains(identifier))) { + continue; + } ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier); Costs costs = getCastSourceIdCosts().get(copy.getSourceId()).get(identifier); @@ -3767,123 +3780,146 @@ public abstract class PlayerImpl implements Player, Serializable { */ protected boolean canPlayCardByAlternateCost(Card sourceObject, ManaOptions availableMana, Ability ability, Game game) { // TODO: Why is the "sourceObject instanceof Permanent" in there? - if (sourceObject != null && !(sourceObject instanceof Permanent)) { - Ability copyAbility; // for alternative cost and reduce tries - for (Ability alternateSourceCostsAbility : sourceObject.getAbilities(game)) { - // if cast for noMana no Alternative costs are allowed - if (alternateSourceCostsAbility instanceof AlternativeSourceCosts) { - if (((AlternativeSourceCosts) alternateSourceCostsAbility).isAvailable(ability, game)) { - if (alternateSourceCostsAbility.getCosts().canPay(ability, ability, playerId, game)) { - ManaCostsImpl manaCosts = new ManaCostsImpl<>(); - for (Cost cost : alternateSourceCostsAbility.getCosts()) { - // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here - if (cost instanceof AlternativeCost) { - if (((AlternativeCost) cost).getCost() instanceof ManaCost) { - manaCosts.add((ManaCost) ((AlternativeCost) cost).getCost()); - } - } else { - if (cost instanceof ManaCost) { - manaCosts.add((ManaCost) cost); - } - } - } - - if (manaCosts.isEmpty()) { - return true; - } else { - if (availableMana == null) { - return true; - } - - // alternative cost reduce - copyAbility = ability.copy(); - copyAbility.clearManaCostsToPay(); - copyAbility.addManaCostsToPay(manaCosts.copy()); - copyAbility.adjustCosts(game); - game.getContinuousEffects().costModification(copyAbility, game); - - // reduced all cost - if (copyAbility.getManaCostsToPay().isEmpty()) { - return true; - } - - for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { - if (availableMana.enough(mana)) { - return true; - } - } - } - } + if (sourceObject == null || (sourceObject instanceof Permanent)) { + return false; + } + Ability copyAbility; // for alternative cost and reduce tries + Set allowedIdentifiers = null; + if (ability instanceof SpellAbility) { + // This returns the set of MageIdentifier that allow to play the card at that timing. + // If MageIdentifier.Default is in the set, everything is allowed to be cast at this time. + // If not, then only the AlternativeSourceCosts with a matching MageIdentifier are allowed at this time. + allowedIdentifiers = ((SpellAbility) ability).spellCanBeActivatedNow(getId(), game); + } + // Try the source specific AlternativeSourceCosts + for (Ability alternateSourceCostsAbility : sourceObject.getAbilities(game)) { + // if cast for noMana no Alternative costs are allowed + if (!(alternateSourceCostsAbility instanceof AlternativeSourceCosts)) { + continue; + } + if (!((AlternativeSourceCosts) alternateSourceCostsAbility).isAvailable(ability, game)) { + continue; + } + if (!alternateSourceCostsAbility.getCosts().canPay(ability, ability, playerId, game)) { + continue; + } + if (allowedIdentifiers != null && !allowedIdentifiers.contains(MageIdentifier.Default) + && !allowedIdentifiers.contains(alternateSourceCostsAbility.getIdentifier())) { + continue; + } + ManaCostsImpl manaCosts = new ManaCostsImpl<>(); + for (Cost cost : alternateSourceCostsAbility.getCosts()) { + // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here + if (cost instanceof AlternativeCost) { + if (((AlternativeCost) cost).getCost() instanceof ManaCost) { + manaCosts.add((ManaCost) ((AlternativeCost) cost).getCost()); + } + } else { + if (cost instanceof ManaCost) { + manaCosts.add((ManaCost) cost); } } } - // controller specific alternate spell costs - for (AlternativeSourceCosts alternateSourceCosts : getAlternativeSourceCosts()) { - if (alternateSourceCosts instanceof Ability) { - if (alternateSourceCosts.isAvailable(ability, game)) { - if (((Ability) alternateSourceCosts).getCosts().canPay(ability, ability, playerId, game)) { - ManaCostsImpl manaCosts = new ManaCostsImpl<>(); - for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) { - // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here - if (cost instanceof AlternativeCost) { - if (((AlternativeCost) cost).getCost() instanceof ManaCost) { - manaCosts.add((ManaCost) ((AlternativeCost) cost).getCost()); - } - } else { - if (cost instanceof ManaCost) { - manaCosts.add((ManaCost) cost); - } - } - } + if (manaCosts.isEmpty()) { + return true; + } else { + if (availableMana == null) { + return true; + } - // Add a copy of the dynamic cost for this ability if there is one. - // E.g. from Kentaro, the Smiling Cat - if (alternateSourceCosts instanceof AlternativeCostSourceAbility) { - DynamicCost dynamicCost = ((AlternativeCostSourceAbility) alternateSourceCosts).getDynamicCost(); - if (dynamicCost != null) { - Cost cost = dynamicCost.getCost(ability, game); - // TODO: I don't know if this first if-check is needed, I don't think any of the dynamics values are alternative costs. - if (cost instanceof AlternativeCost) { - cost = ((AlternativeCost) cost).getCost(); - } - if (cost instanceof ManaCost) { - manaCosts.add((ManaCost) cost); - } - } - } + // alternative cost reduce + copyAbility = ability.copy(); + copyAbility.clearManaCostsToPay(); + copyAbility.addManaCostsToPay(manaCosts.copy()); + copyAbility.adjustCosts(game); + game.getContinuousEffects().costModification(copyAbility, game); - if (manaCosts.isEmpty()) { - return true; - } else { - // TODO: Why is it returning true if availableMana == null, one would think it should return false - if (availableMana == null) { - return true; - } + // reduced all cost + if (copyAbility.getManaCostsToPay().isEmpty()) { + return true; + } - // alternative cost reduce - copyAbility = ability.copy(); - copyAbility.clearManaCostsToPay(); - // TODO: IDE warning: - // Unchecked assignment: 'mage.abilities.costs.mana.ManaCosts' to - // 'java.util.Collection'. - // Reason: 'manaCosts' has raw type, so result of copy is erased - copyAbility.addManaCostsToPay(manaCosts.copy()); - copyAbility.adjustCosts(game); - game.getContinuousEffects().costModification(copyAbility, game); + for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { + if (availableMana.enough(mana)) { + return true; + } + } + } + } - // reduced all cost - if (copyAbility.getManaCostsToPay().isEmpty()) { - return true; - } + // controller specific alternate spell costs + for (AlternativeSourceCosts alternateSourceCosts : getAlternativeSourceCosts()) { + if (!(alternateSourceCosts instanceof Ability)) { + continue; + } + if (!alternateSourceCosts.isAvailable(ability, game)) { + continue; + } + if (!((Ability) alternateSourceCosts).getCosts().canPay(ability, ability, playerId, game)) { + continue; + } + if (allowedIdentifiers != null && !allowedIdentifiers.contains(MageIdentifier.Default) + && !allowedIdentifiers.contains(((Ability) alternateSourceCosts).getIdentifier())) { + continue; + } + ManaCostsImpl manaCosts = new ManaCostsImpl<>(); + for (Cost cost : ((Ability) alternateSourceCosts).getCosts()) { + // AlternativeCost2 replaced by real cost on activate, so getPlayable need to extract that costs here + if (cost instanceof AlternativeCost) { + if (((AlternativeCost) cost).getCost() instanceof ManaCost) { + manaCosts.add((ManaCost) ((AlternativeCost) cost).getCost()); + } + } else { + if (cost instanceof ManaCost) { + manaCosts.add((ManaCost) cost); + } + } + } - for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { - if (availableMana.enough(mana)) { - return true; - } - } - } - } + // Add a copy of the dynamic cost for this ability if there is one. + // E.g. from Kentaro, the Smiling Cat + if (alternateSourceCosts instanceof AlternativeCostSourceAbility) { + DynamicCost dynamicCost = ((AlternativeCostSourceAbility) alternateSourceCosts).getDynamicCost(); + if (dynamicCost != null) { + Cost cost = dynamicCost.getCost(ability, game); + // TODO: I don't know if this first if-check is needed, I don't think any of the dynamics values are alternative costs. + if (cost instanceof AlternativeCost) { + cost = ((AlternativeCost) cost).getCost(); + } + if (cost instanceof ManaCost) { + manaCosts.add((ManaCost) cost); + } + } + } + + if (manaCosts.isEmpty()) { + return true; + } else { + // TODO: Why is it returning true if availableMana == null, one would think it should return false + if (availableMana == null) { + return true; + } + + // alternative cost reduce + copyAbility = ability.copy(); + copyAbility.clearManaCostsToPay(); + // TODO: IDE warning: + // Unchecked assignment: 'mage.abilities.costs.mana.ManaCosts' to + // 'java.util.Collection'. + // Reason: 'manaCosts' has raw type, so result of copy is erased + copyAbility.addManaCostsToPay(manaCosts.copy()); + copyAbility.adjustCosts(game); + game.getContinuousEffects().costModification(copyAbility, game); + + // reduced all cost + if (copyAbility.getManaCostsToPay().isEmpty()) { + return true; + } + + for (Mana mana : copyAbility.getManaCostsToPay().getOptions()) { + if (availableMana.enough(mana)) { + return true; } } }