From 574d7f91a5637593b258a62164a82539975c9682 Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Sat, 9 Aug 2025 17:53:43 -0500 Subject: [PATCH] fix Yasharn, Implacable Earth and Angel of Jubilation (#13753) * Fix Angel of Jubilation and Yasharn, Implacable Earth * canPaySacrificeCost filter was not checking if the source ability was a spell or activated ability * Create common CantPayLifeOrSacrificeEffect * add some docs for CantPayLifeOrSacrificeEffect * change player pay life restrictions and remove player sacrifice cost filter * pay life cost restriction is now an enum set so multiple effects apply together * sacrifice cost filter was removed and replaced with PAY_SACRIFICE_COST event * convert CantPayLifeEffect to CantPayLifeOrSacrificeAbility * Changed to combine life restriction and sacrifice cost restriction * update bargain ability cost adjustors using canPay * fix Thran Portal * Effect was incorrectly adjusting the cost of mana abilities on itself. * Fixed ability adding type to itself during ETB * Add additional tests * update PayLifeCostRestrictions to be mutually exclusive --- .../src/mage/cards/a/AngelOfJubilation.java | 82 +------- .../src/mage/cards/b/BiteDownOnCrime.java | 4 +- Mage.Sets/src/mage/cards/h/HamletGlutton.java | 2 +- Mage.Sets/src/mage/cards/i/IceOut.java | 2 +- .../src/mage/cards/j/JohannsStopgap.java | 2 +- Mage.Sets/src/mage/cards/k/KarnsSylex.java | 37 +--- Mage.Sets/src/mage/cards/t/ThranPortal.java | 83 +++++--- .../mage/cards/y/YasharnImplacableEarth.java | 146 +------------ .../cards/abilities/keywords/BargainTest.java | 16 ++ .../continuous/AngelOfJubilationTest.java | 128 ++++++++++-- .../test/cards/single/dmu/KarnsSylexTest.java | 23 ++- .../cards/single/dmu/ThranPortalTest.java | 2 - .../znr/YasharnImplacableEarthTest.java | 193 ++++++++++++++++-- .../java/org/mage/test/player/TestPlayer.java | 18 +- .../common/CantPayLifeOrSacrificeAbility.java | 151 ++++++++++++++ .../main/java/mage/game/events/GameEvent.java | 7 + Mage/src/main/java/mage/players/Player.java | 33 +-- .../main/java/mage/players/PlayerImpl.java | 68 +++--- 18 files changed, 610 insertions(+), 387 deletions(-) create mode 100644 Mage/src/main/java/mage/abilities/common/CantPayLifeOrSacrificeAbility.java diff --git a/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java b/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java index 535752c85f4..8fb4bcc2cca 100644 --- a/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java +++ b/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java @@ -1,23 +1,16 @@ package mage.cards.a; import mage.MageInt; -import mage.abilities.Ability; +import mage.abilities.common.CantPayLifeOrSacrificeAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.Cost; -import mage.abilities.costs.common.SacrificeTargetCost; -import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.common.continuous.BoostControlledEffect; -import mage.abilities.effects.common.cost.CostModificationEffectImpl; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.filter.Filter; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; import mage.filter.StaticFilters; -import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.Predicates; -import mage.game.Game; -import mage.players.Player; import java.util.UUID; @@ -39,9 +32,7 @@ public final class AngelOfJubilation extends CardImpl { this.addAbility(new SimpleStaticAbility(new BoostControlledEffect(1, 1, Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT_CREATURES_NON_BLACK, true))); // Players can't pay life or sacrifice creatures to cast spells or activate abilities. - Ability ability = new SimpleStaticAbility(new AngelOfJubilationEffect()); - ability.addEffect(new AngelOfJubilationSacrificeFilterEffect()); - this.addAbility(ability); + this.addAbility(new CantPayLifeOrSacrificeAbility(StaticFilters.FILTER_PERMANENT_CREATURES)); } private AngelOfJubilation(final AngelOfJubilation card) { @@ -53,66 +44,3 @@ public final class AngelOfJubilation extends CardImpl { return new AngelOfJubilation(this); } } - -class AngelOfJubilationEffect extends ContinuousEffectImpl { - - AngelOfJubilationEffect() { - super(Duration.WhileOnBattlefield, Layer.PlayerEffects, SubLayer.NA, Outcome.Detriment); - staticText = "Players can't pay life or sacrifice creatures to cast spells"; - } - - private AngelOfJubilationEffect(final AngelOfJubilationEffect effect) { - super(effect); - } - - @Override - public AngelOfJubilationEffect copy() { - return new AngelOfJubilationEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { - Player player = game.getPlayer(playerId); - player.setPayLifeCostLevel(Player.PayLifeCostLevel.nonSpellnonActivatedAbilities); - player.setCanPaySacrificeCostFilter(new FilterCreaturePermanent()); - } - return true; - } -} - -class AngelOfJubilationSacrificeFilterEffect extends CostModificationEffectImpl { - - AngelOfJubilationSacrificeFilterEffect() { - super(Duration.WhileOnBattlefield, Outcome.Detriment, CostModificationType.SET_COST); - staticText = "or activate abilities"; - } - - protected AngelOfJubilationSacrificeFilterEffect(AngelOfJubilationSacrificeFilterEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source, Ability abilityToModify) { - for (Cost cost : abilityToModify.getCosts()) { - if (cost instanceof SacrificeTargetCost) { - SacrificeTargetCost sacrificeCost = (SacrificeTargetCost) cost; - Filter filter = sacrificeCost.getTargets().get(0).getFilter(); - filter.add(Predicates.not(CardType.CREATURE.getPredicate())); - } - } - return true; - } - - @Override - public boolean applies(Ability abilityToModify, Ability source, Game game) { - return (abilityToModify.isActivatedAbility() || abilityToModify.getAbilityType() == AbilityType.SPELL) - && game.getState().getPlayersInRange(source.getControllerId(), game).contains(abilityToModify.getControllerId()); - } - - @Override - public AngelOfJubilationSacrificeFilterEffect copy() { - return new AngelOfJubilationSacrificeFilterEffect(this); - } - -} diff --git a/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java b/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java index 067cabe2f2a..3daa5a7fd14 100644 --- a/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java +++ b/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java @@ -12,11 +12,9 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; -import mage.filter.StaticFilters; import mage.game.Game; import mage.target.TargetPermanent; import mage.target.common.TargetControlledCreaturePermanent; -import mage.target.common.TargetCreaturePermanent; import mage.util.CardUtil; import java.util.UUID; @@ -65,7 +63,7 @@ enum BiteDownOnCrimeAdjuster implements CostAdjuster { @Override public void reduceCost(Ability ability, Game game) { if (CollectedEvidenceCondition.instance.apply(game, ability) - || (game.inCheckPlayableState() && collectEvidenceCost.canPay(ability, null, ability.getControllerId(), game))) { + || (game.inCheckPlayableState() && collectEvidenceCost.canPay(ability, ability, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); } } diff --git a/Mage.Sets/src/mage/cards/h/HamletGlutton.java b/Mage.Sets/src/mage/cards/h/HamletGlutton.java index 703877d465b..376bef9b089 100644 --- a/Mage.Sets/src/mage/cards/h/HamletGlutton.java +++ b/Mage.Sets/src/mage/cards/h/HamletGlutton.java @@ -63,7 +63,7 @@ enum HamletGluttonAdjuster implements CostAdjuster { @Override public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) - || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { + || (game.inCheckPlayableState() && bargainCost.canPay(ability, ability, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); } } diff --git a/Mage.Sets/src/mage/cards/i/IceOut.java b/Mage.Sets/src/mage/cards/i/IceOut.java index 6d4dcf0eeb3..97fa2d5a392 100644 --- a/Mage.Sets/src/mage/cards/i/IceOut.java +++ b/Mage.Sets/src/mage/cards/i/IceOut.java @@ -54,7 +54,7 @@ enum IceOutAdjuster implements CostAdjuster { @Override public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) - || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { + || (game.inCheckPlayableState() && bargainCost.canPay(ability, ability, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 1); } } diff --git a/Mage.Sets/src/mage/cards/j/JohannsStopgap.java b/Mage.Sets/src/mage/cards/j/JohannsStopgap.java index fc34f2aa974..ff579d29591 100644 --- a/Mage.Sets/src/mage/cards/j/JohannsStopgap.java +++ b/Mage.Sets/src/mage/cards/j/JohannsStopgap.java @@ -56,7 +56,7 @@ enum JohannsStopgapAdjuster implements CostAdjuster { @Override public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) - || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { + || (game.inCheckPlayableState() && bargainCost.canPay(ability, ability, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); } } diff --git a/Mage.Sets/src/mage/cards/k/KarnsSylex.java b/Mage.Sets/src/mage/cards/k/KarnsSylex.java index df61aadcc9b..f4e19c66515 100644 --- a/Mage.Sets/src/mage/cards/k/KarnsSylex.java +++ b/Mage.Sets/src/mage/cards/k/KarnsSylex.java @@ -2,21 +2,22 @@ package mage.cards.k; import mage.abilities.Ability; import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.common.CantPayLifeOrSacrificeAbility; import mage.abilities.common.EntersBattlefieldTappedAbility; -import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.common.ExileSourceCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.OneShotEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.Outcome; +import mage.constants.SuperType; import mage.filter.common.FilterNonlandPermanent; import mage.filter.predicate.mageobject.ManaValuePredicate; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.players.Player; import mage.util.CardUtil; import java.util.UUID; @@ -33,7 +34,7 @@ public class KarnsSylex extends CardImpl { this.addAbility(new EntersBattlefieldTappedAbility()); // Players can’t pay life to cast spells or to activate abilities that aren’t mana abilities. - this.addAbility(new SimpleStaticAbility(new KarnsSylexEffect())); + this.addAbility(new CantPayLifeOrSacrificeAbility(true, null)); // {X}, {T}, Exile Karn’s Sylex: Destroy each nonland permanent with mana value X or less. Activate only as a sorcery. Ability ability = new ActivateAsSorceryActivatedAbility(new KarnsSylexDestroyEffect(), new ManaCostsImpl<>("{X}")); @@ -52,32 +53,6 @@ public class KarnsSylex extends CardImpl { } } -class KarnsSylexEffect extends ContinuousEffectImpl { - - KarnsSylexEffect() { - super(Duration.WhileOnBattlefield, Layer.PlayerEffects, SubLayer.NA, Outcome.Detriment); - staticText = "Players can't pay life to cast spells or to activate abilities that aren't mana abilities"; - } - - private KarnsSylexEffect(final KarnsSylexEffect effect) { - super(effect); - } - - @Override - public KarnsSylexEffect copy() { - return new KarnsSylexEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { - Player player = game.getPlayer(playerId); - player.setPayLifeCostLevel(Player.PayLifeCostLevel.onlyManaAbilities); - } - return true; - } -} - class KarnsSylexDestroyEffect extends OneShotEffect { KarnsSylexDestroyEffect() { diff --git a/Mage.Sets/src/mage/cards/t/ThranPortal.java b/Mage.Sets/src/mage/cards/t/ThranPortal.java index 01dd82ddf01..fda95f376fd 100644 --- a/Mage.Sets/src/mage/cards/t/ThranPortal.java +++ b/Mage.Sets/src/mage/cards/t/ThranPortal.java @@ -7,10 +7,8 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.YouControlPermanentCondition; import mage.abilities.costs.common.PayLifeCost; import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ChooseBasicLandTypeEffect; -import mage.abilities.effects.common.continuous.AddChosenSubtypeEffect; -import mage.abilities.effects.common.cost.CostModificationEffectImpl; -import mage.abilities.effects.common.enterAttribute.EnterAttributeAddChosenSubtypeEffect; import mage.abilities.mana.*; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -21,6 +19,7 @@ import mage.game.Game; import mage.game.permanent.Permanent; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -47,16 +46,15 @@ public class ThranPortal extends CardImpl { this.addAbility(new EntersBattlefieldTappedUnlessAbility(condition).addHint(condition.getHint())); // As Thran Portal enters the battlefield, choose a basic land type. - // Thran Portal is the chosen type in addition to its other types. AsEntersBattlefieldAbility chooseLandTypeAbility = new AsEntersBattlefieldAbility(new ChooseBasicLandTypeEffect(Outcome.AddAbility)); - chooseLandTypeAbility.addEffect(new EnterAttributeAddChosenSubtypeEffect()); // While it enters + chooseLandTypeAbility.addEffect(new ThranPortalAddSubtypeEnteringEffect()); this.addAbility(chooseLandTypeAbility); - this.addAbility(new SimpleStaticAbility(new AddChosenSubtypeEffect())); // While on the battlefield + + // Thran Portal is the chosen type in addition to its other types. + this.addAbility(new SimpleStaticAbility(new ThranPortalManaAbilityContinuousEffect())); // Mana abilities of Thran Portal cost an additional 1 life to activate. - // This also adds the mana ability Ability ability = new SimpleStaticAbility(new ThranPortalAdditionalCostEffect()); - ability.addEffect(new ThranPortalManaAbilityContinousEffect()); this.addAbility(ability); } @@ -70,7 +68,34 @@ public class ThranPortal extends CardImpl { } } -class ThranPortalManaAbilityContinousEffect extends ContinuousEffectImpl { +class ThranPortalAddSubtypeEnteringEffect extends OneShotEffect { + + public ThranPortalAddSubtypeEnteringEffect() { + super(Outcome.Benefit); + } + + protected ThranPortalAddSubtypeEnteringEffect(final ThranPortalAddSubtypeEnteringEffect effect) { + super(effect); + } + + @Override + public ThranPortalAddSubtypeEnteringEffect copy() { + return new ThranPortalAddSubtypeEnteringEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent thranPortal = game.getPermanentEntering(source.getSourceId()); + SubType choice = SubType.byDescription((String) game.getState().getValue(source.getSourceId().toString() + ChooseBasicLandTypeEffect.VALUE_KEY)); + if (thranPortal != null && choice != null) { + thranPortal.addSubType(choice); + return true; + } + return false; + } +} + +class ThranPortalManaAbilityContinuousEffect extends ContinuousEffectImpl { private static final Map abilityMap = new HashMap() {{ put(SubType.PLAINS, new WhiteManaAbility()); @@ -80,18 +105,18 @@ class ThranPortalManaAbilityContinousEffect extends ContinuousEffectImpl { put(SubType.FOREST, new GreenManaAbility()); }}; - public ThranPortalManaAbilityContinousEffect() { + public ThranPortalManaAbilityContinuousEffect() { super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Neutral); - staticText = "mana abilities of {this} cost an additional 1 life to activate"; + staticText = "{this} is the chosen type in addition to its other types."; } - private ThranPortalManaAbilityContinousEffect(final ThranPortalManaAbilityContinousEffect effect) { + private ThranPortalManaAbilityContinuousEffect(final ThranPortalManaAbilityContinuousEffect effect) { super(effect); } @Override - public ThranPortalManaAbilityContinousEffect copy() { - return new ThranPortalManaAbilityContinousEffect(this); + public ThranPortalManaAbilityContinuousEffect copy() { + return new ThranPortalManaAbilityContinuousEffect(this); } @Override @@ -143,10 +168,11 @@ class ThranPortalManaAbilityContinousEffect extends ContinuousEffectImpl { } } -class ThranPortalAdditionalCostEffect extends CostModificationEffectImpl { +class ThranPortalAdditionalCostEffect extends ContinuousEffectImpl { ThranPortalAdditionalCostEffect() { - super(Duration.WhileOnBattlefield, Outcome.Benefit, CostModificationType.INCREASE_COST); + super(Duration.WhileOnBattlefield, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit); + staticText = "mana abilities of {this} cost an additional 1 life to activate"; } private ThranPortalAdditionalCostEffect(final ThranPortalAdditionalCostEffect effect) { @@ -159,17 +185,22 @@ class ThranPortalAdditionalCostEffect extends CostModificationEffectImpl { } @Override - public boolean apply(Game game, Ability source, Ability abilityToModify) { - abilityToModify.addCost(new PayLifeCost(1)); - return true; - } - - @Override - public boolean applies(Ability abilityToModify, Ability source, Game game) { - if (!abilityToModify.getSourceId().equals(source.getSourceId())) { + public boolean apply(Game game, Ability source) { + Permanent thranPortal = game.getPermanent(source.getSourceId()); + if (thranPortal == null) { return false; } - - return abilityToModify instanceof ManaAbility; + List abilities = thranPortal.getAbilities(game); + if (abilities.isEmpty()) { + return false; + } + boolean result = false; + for (Ability ability : abilities) { + if (ability.isManaAbility()) { + ability.addCost(new PayLifeCost(1)); + result = true; + } + } + return result; } } diff --git a/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java b/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java index 6f5f5a44238..ee9b54f81e4 100644 --- a/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java +++ b/Mage.Sets/src/mage/cards/y/YasharnImplacableEarth.java @@ -1,34 +1,22 @@ package mage.cards.y; -import java.util.Optional; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.Cost; -import mage.abilities.costs.common.SacrificeTargetCost; import mage.abilities.assignment.common.SubTypeAssignment; +import mage.abilities.common.CantPayLifeOrSacrificeAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; import mage.cards.*; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.target.common.TargetCardInLibrary; import java.util.UUID; -import mage.MageObject; -import mage.abilities.costs.common.PayLifeCost; -import mage.abilities.costs.common.PayVariableLifeCost; -import mage.abilities.costs.common.SacrificeAllCost; -import mage.abilities.costs.common.SacrificeAttachedCost; -import mage.abilities.costs.common.SacrificeAttachmentCost; -import mage.abilities.costs.common.SacrificeSourceCost; -import mage.abilities.costs.common.SacrificeXTargetCost; -import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; -import mage.filter.Filter; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; /** * @author TheElk801 @@ -52,8 +40,7 @@ public final class YasharnImplacableEarth extends CardImpl { )); // Players can't pay life or sacrifice nonland permanents to cast spells or activate abilities. - Ability ability = new SimpleStaticAbility(new YasharnImplacableEarthEffect()); - this.addAbility(ability); + this.addAbility(new CantPayLifeOrSacrificeAbility(StaticFilters.FILTER_PERMANENTS_NON_LAND)); } private YasharnImplacableEarth(final YasharnImplacableEarth card) { @@ -111,122 +98,3 @@ class YasharnImplacableEarthTarget extends TargetCardInLibrary { return subTypeAssigner.getRoleCount(cards, game) >= cards.size(); } } - -class YasharnImplacableEarthEffect extends ContinuousRuleModifyingEffectImpl { - - YasharnImplacableEarthEffect() { - super(Duration.WhileOnBattlefield, Outcome.Neutral); - staticText = "Players can't pay life or sacrifice nonland permanents to cast spells or activate abilities"; - } - - private YasharnImplacableEarthEffect(final YasharnImplacableEarthEffect effect) { - super(effect); - } - - @Override - public YasharnImplacableEarthEffect copy() { - return new YasharnImplacableEarthEffect(this); - } - - @Override - public String getInfoMessage(Ability source, GameEvent event, Game game) { - MageObject mageObject = game.getObject(source); - if (mageObject != null) { - return "Players can't pay life or sacrifice nonland permanents to cast spells or activate abilities. (" + mageObject.getIdName() + ")."; - } - return null; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ACTIVATE_ABILITY - || event.getType() == GameEvent.EventType.CAST_SPELL; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId()); - if (event.getType() == GameEvent.EventType.ACTIVATE_ABILITY && permanent == null) { - return false; - } - - boolean canTargetLand = true; - Optional ability = game.getAbility(event.getTargetId(), event.getSourceId()); - if (!ability.isPresent()) { - return false; - } - - for (Cost cost : ability.get().getCosts()) { - if (cost instanceof PayLifeCost - || cost instanceof PayVariableLifeCost) { - return true; // can't pay with life - } - if (cost instanceof SacrificeSourceCost - && !permanent.isLand(game)) { - return true; - } - if (cost instanceof SacrificeTargetCost) { - SacrificeTargetCost sacrificeCost = (SacrificeTargetCost) cost; - Filter filter = sacrificeCost.getTargets().get(0).getFilter(); - for (Object predicate : filter.getPredicates()) { - if (predicate instanceof CardType.CardTypePredicate) { - if (!predicate.toString().equals("CardType(Land)")) { - canTargetLand = false; - } - } - } - return !canTargetLand; // must be nonland target - } - if (cost instanceof SacrificeAllCost) { - SacrificeAllCost sacrificeAllCost = (SacrificeAllCost) cost; - Filter filter = sacrificeAllCost.getTargets().get(0).getFilter(); - for (Object predicate : filter.getPredicates()) { - if (predicate instanceof CardType.CardTypePredicate) { - if (!predicate.toString().equals("CardType(Land)")) { - canTargetLand = false; - } - } - } - return !canTargetLand; // must be nonland target - } - if (cost instanceof SacrificeAttachedCost) { - SacrificeAttachedCost sacrificeAllCost = (SacrificeAttachedCost) cost; - Filter filter = sacrificeAllCost.getTargets().get(0).getFilter(); - for (Object predicate : filter.getPredicates()) { - if (predicate instanceof CardType.CardTypePredicate) { - if (!predicate.toString().equals("CardType(Land)")) { - canTargetLand = false; - } - } - } - return !canTargetLand; // must be nonland target - } - if (cost instanceof SacrificeAttachmentCost) { - SacrificeAttachmentCost sacrificeAllCost = (SacrificeAttachmentCost) cost; - Filter filter = sacrificeAllCost.getTargets().get(0).getFilter(); - for (Object predicate : filter.getPredicates()) { - if (predicate instanceof CardType.CardTypePredicate) { - if (!predicate.toString().equals("CardType(Land)")) { - canTargetLand = false; - } - } - } - return !canTargetLand; // must be nonland target - } - - if (cost instanceof SacrificeXTargetCost) { - SacrificeXTargetCost sacrificeCost = (SacrificeXTargetCost) cost; - Filter filter = sacrificeCost.getFilter(); - for (Object predicate : filter.getPredicates()) { - if (predicate instanceof CardType.CardTypePredicate) { - if (!predicate.toString().equals("CardType(Land)")) { - canTargetLand = false; - } - } - } - return !canTargetLand; // must be nonland target - } - } - return false; - } -} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BargainTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BargainTest.java index 98b5e6a3b1b..59885e2e729 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BargainTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BargainTest.java @@ -337,4 +337,20 @@ public class BargainTest extends CardTestPlayerBase { assertLife(playerA, 20 + 3); assertTappedCount("Forest", true, 7); } + + @Test + public void testCantBargainWithRestriction() { + setStrictChooseMode(true); + // Players can’t pay life or sacrifice nonland permanents to cast spells or activate abilities. + addCard(Zone.BATTLEFIELD, playerB, "Yasharn, Implacable Earth"); + addCard(Zone.HAND, playerA, glutton); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, relic); + + checkPlayableAbility("restricted by Yasharn", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hamlet Glutton", false); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/AngelOfJubilationTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/AngelOfJubilationTest.java index 2655cdda368..54852c869d3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/AngelOfJubilationTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/AngelOfJubilationTest.java @@ -1,7 +1,13 @@ package org.mage.test.cards.continuous; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.permanent.token.FoodToken; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -18,12 +24,14 @@ import org.mage.test.serverside.base.CardTestPlayerBase; */ public class AngelOfJubilationTest extends CardTestPlayerBase { + public static final String angelOfJubilation = "Angel of Jubilation"; + /** * Tests boosting other non black creatures */ @Test public void testBoost() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerA, "Devout Chaplain"); addCard(Zone.BATTLEFIELD, playerA, "Corpse Traders"); @@ -33,7 +41,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { assertLife(playerA, 20); assertLife(playerB, 20); - assertPowerToughness(playerA, "Angel of Jubilation", 3, 3); + assertPowerToughness(playerA, angelOfJubilation, 3, 3); assertPowerToughness(playerA, "Devout Chaplain", 3, 3); assertPowerToughness(playerA, "Corpse Traders", 3, 3); } @@ -43,14 +51,14 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { */ @Test public void testNoBoostOnBattlefieldLeave() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerA, "Devout Chaplain"); addCard(Zone.BATTLEFIELD, playerA, "Corpse Traders"); addCard(Zone.HAND, playerA, "Lightning Bolt"); addCard(Zone.BATTLEFIELD, playerA, "Mountain"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Angel of Jubilation"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", angelOfJubilation); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -58,14 +66,14 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { assertLife(playerA, 20); assertLife(playerB, 20); - assertPermanentCount(playerA, "Angel of Jubilation", 0); + assertPermanentCount(playerA, angelOfJubilation, 0); assertPowerToughness(playerA, "Devout Chaplain", 2, 2); assertPowerToughness(playerA, "Corpse Traders", 3, 3); } @Test public void testOpponentCantSacrificeCreatures() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk"); addCard(Zone.BATTLEFIELD, playerB, "Corpse Traders"); @@ -81,7 +89,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { @Test public void testOpponentCanSacrificeNonCreaturePermanents() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerA, "Savannah Lions"); addCard(Zone.BATTLEFIELD, playerB, "Barrin, Master Wizard"); addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk"); @@ -89,20 +97,20 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Food Chain"); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{2}, Sacrifice a permanent: Return target creature to its owner's hand."); - addTarget(playerB, "Angel of Jubilation"); // return to hand + addTarget(playerB, angelOfJubilation); // return to hand setChoice(playerB, "Food Chain"); // sacrifice cost setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); - assertPermanentCount(playerA, "Angel of Jubilation", 0); + assertPermanentCount(playerA, angelOfJubilation, 0); assertPermanentCount(playerB, "Food Chain", 0); } @Test public void testOpponentCantSacrificeCreaturesAsPartOfPermanentsOptions() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerB, "Barrin, Master Wizard"); addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk"); addCard(Zone.BATTLEFIELD, playerB, "Llanowar Elves", 2); @@ -115,13 +123,13 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.END_TURN); execute(); - assertPermanentCount(playerA, "Angel of Jubilation", 1); + assertPermanentCount(playerA, angelOfJubilation, 1); assertPermanentCount(playerB, "Nantuko Husk", 1); } @Test public void testOpponentCantSacrificeAll() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerB, "Nantuko Husk"); addCard(Zone.BATTLEFIELD, playerB, "Corpse Traders"); addCard(Zone.HAND, playerB, "Soulblast"); @@ -142,7 +150,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { @Test public void testOpponentCantSacrificeCreatureSource() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerB, "Children of Korlis"); checkPlayableAbility("Can't sac", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Sacrifice", false); @@ -155,7 +163,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { @Test public void testOpponentCanSacrificeAllLands() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerB, "Tomb of Urami"); addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4); @@ -169,7 +177,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { @Test public void testOpponentCanSacrificeNonCreatureSource() { - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerA, "Tundra"); addCard(Zone.BATTLEFIELD, playerB, "Wasteland"); @@ -195,7 +203,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { setStrictChooseMode(true); // Other nonblack creatures you control get +1/+1. // Players can't pay life or sacrifice creatures to cast spells or activate abilities - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); // Indestructible @@ -234,7 +242,7 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { setStrictChooseMode(true); // Other nonblack creatures you control get +1/+1. // Players can't pay life or sacrifice creatures to cast spells or activate abilities - addCard(Zone.BATTLEFIELD, playerA, "Angel of Jubilation"); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); // Pay 7 life: Draw seven cards. addCard(Zone.BATTLEFIELD, playerB, "Griselbrand"); @@ -244,4 +252,90 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); } + + @Test + public void testCanSacrificeTriggeredAbility() { + /* + Unscrupulous Contractor + {2}{B} + Creature — Human Assassin + When this creature enters, you may sacrifice a creature. When you do, target player draws two cards and loses 2 life. + Plot {2}{B} + 3/2 + */ + String contractor = "Unscrupulous Contractor"; + /* + Bear Cub + {1}{G} + Creature - Bear + 2/2 + */ + String cub = "Bear Cub"; + + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); + addCard(Zone.HAND, playerA, contractor); + addCard(Zone.HAND, playerB, contractor); + addCard(Zone.BATTLEFIELD, playerA, cub); + addCard(Zone.BATTLEFIELD, playerB, cub); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, contractor); + setChoice(playerA, true); + setChoice(playerA, cub); + addTarget(playerA, playerA); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, contractor); + setChoice(playerB, true); + setChoice(playerB, cub); + addTarget(playerB, playerB); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 2); + assertLife(playerA, 20 - 2); + assertGraveyardCount(playerA, cub, 1); + + assertHandCount(playerB, 1 + 2); //draw + contractor effect + assertLife(playerB, 20 - 2); + assertGraveyardCount(playerB, cub, 1); + } + + @Test + public void canSacToMondrakWithArtifacts() { + setStrictChooseMode(true); + //Mondrak, Glory Dominus + //{2}{W}{W} + //Legendary Creature — Phyrexian Horror + //If one or more tokens would be created under your control, twice that many of those tokens are created instead. + //{1}{W/P}{W/P}, Sacrifice two other artifacts and/or creatures: Put an indestructible counter on Mondrak. + String mondrak = "Mondrak, Glory Dominus"; + Ability ability = new SimpleActivatedAbility( + Zone.ALL, + new CreateTokenEffect(new FoodToken(), 2), + new ManaCostsImpl<>("") + ); + addCustomCardWithAbility("Token-maker", playerA, ability); + + addCard(Zone.BATTLEFIELD, playerA, mondrak); + addCard(Zone.BATTLEFIELD, playerA, angelOfJubilation); + addCard(Zone.BATTLEFIELD, playerA, "Bear Cub", 2); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + + checkPlayableAbility("Can't activate Mondrak", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W/P}{W/P}, Sacrifice", false); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "create two"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W/P}{W/P}, Sacrifice"); + setChoice(playerA, "Food Token", 2); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, mondrak, CounterType.INDESTRUCTIBLE, 1); + assertPermanentCount(playerA, "Bear Cub", 2); + assertPermanentCount(playerA, "Food Token", 2); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java index 5ad1afd3b6f..3f76a1a73b6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java @@ -2,7 +2,6 @@ package org.mage.test.cards.single.dmu; import mage.constants.PhaseStep; import mage.constants.Zone; -import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -76,4 +75,26 @@ public class KarnsSylexTest extends CardTestPlayerBase { assertLife(playerB, 20 - 3); assertGraveyardCount(playerA, "Lightning Bolt", 1); } + + /** + * Test that it does not work with mana abilities, e.g. Thran Portal, with Yasharn, Implacable Earth. + */ + @Test + public void blockedManaAbilitiesWithYasharn() { + addCard(Zone.HAND, playerA, "Thran Portal"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, karnsSylex); + addCard(Zone.BATTLEFIELD, playerA, "Yasharn, Implacable Earth"); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thran Portal"); + setChoice(playerA, "Thran"); + setChoice(playerA, "Mountain"); + + checkPlayableAbility("restricted by Yasharn", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", false); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertLife(playerA, 20); + assertLife(playerB, 20); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/ThranPortalTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/ThranPortalTest.java index 214a6985c81..881f45d1e0d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/ThranPortalTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/ThranPortalTest.java @@ -5,8 +5,6 @@ import mage.constants.Zone; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; -import java.lang.annotation.Target; - /** * {@link mage.cards.t.ThranPortal Thran Portal} * Land Gate diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java index 2daabf2511d..a17b75ab343 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/znr/YasharnImplacableEarthTest.java @@ -1,9 +1,16 @@ package org.mage.test.cards.single.znr; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.keyword.FlyingAbility; import mage.constants.PhaseStep; import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.permanent.token.FoodToken; +import mage.game.permanent.token.TreasureToken; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -23,21 +30,26 @@ public class YasharnImplacableEarthTest extends CardTestPlayerBase { * Test that players can't pay life to cast a spell. */ @Test - @Ignore public void cantPayLifeToCast() { // {W}{B} // As an additional cost to cast this spell, pay 5 life or sacrifice a creature or enchantment. // Destroy target creature. addCard(Zone.HAND, playerA, "Final Payment"); + //{4}{B/P}{B/P}{B/P} + //Legendary Creature — Phyrexian Horror Minion + //2/2 + //Lifelink + //For each {B} in a cost, you may pay 2 life rather than pay that mana. + //Whenever you cast a black spell, put a +1/+1 counter on K'rrik. + addCard(Zone.HAND, playerA, "K'rrik, Son of Yawgmoth"); addCard(Zone.BATTLEFIELD, playerA, yasharn); - addCard(Zone.BATTLEFIELD, playerA, "Swamp"); - addCard(Zone.BATTLEFIELD, playerA, "Plains"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion"); setStrictChooseMode(true); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Final Payment", yasharn); - setChoice(playerA, "No"); - setChoice(playerA, "Silvercoat Lion"); + checkPlayableAbility("Can't cast Final Payment", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Final Payment", false); + checkPlayableAbility("Can't cast K'rrik", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast K'rrik, Son of Yawgmoth", false); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); @@ -71,22 +83,20 @@ public class YasharnImplacableEarthTest extends CardTestPlayerBase { * Test that players can't sacrifice a nonland permanent to cast a spell. */ @Test - @Ignore public void cantSacrificeNonlandToCast() { // {1}{B} // As an additional cost to cast this spell, sacrifice an artifact or creature. // Draw two cards and create a Treasure token. addCard(Zone.HAND, playerA, "Deadly Dispute"); addCard(Zone.BATTLEFIELD, playerA, yasharn); + addCard(Zone.BATTLEFIELD, playerA, "Bear Cub"); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); setStrictChooseMode(true); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Deadly Dispute"); - setChoice(playerA, yasharn); + checkPlayableAbility("Can't cast Deadly Dispute", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Deadly Dispute", false); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); - assertPermanentCount(playerA, "Treasure Token", 0); } /** @@ -117,9 +127,13 @@ public class YasharnImplacableEarthTest extends CardTestPlayerBase { */ @Test public void canSacrificeLandToCast() { + //Crop Rotation + //{G} + //As an additional cost to cast this spell, sacrifice a land. + //Search your library for a land card, put that card onto the battlefield, then shuffle. + addCard(Zone.HAND, playerA, "Crop Rotation"); addCard(Zone.BATTLEFIELD, playerA, yasharn); addCard(Zone.BATTLEFIELD, playerA, "Forest"); - addCard(Zone.HAND, playerA, "Crop Rotation"); addCard(Zone.LIBRARY, playerA, "Mountain"); setStrictChooseMode(true); @@ -156,4 +170,159 @@ public class YasharnImplacableEarthTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Island", 1); assertGraveyardCount(playerA, "Evolving Wilds", 1); } + + /** + * Test that a player cannot sacrifice artifacts or creatures to activate abilities + */ + @Test + public void cantSacToMondrakWithArtifacts() { + setStrictChooseMode(true); + //Mondrak, Glory Dominus + //{2}{W}{W} + //Legendary Creature — Phyrexian Horror + //If one or more tokens would be created under your control, twice that many of those tokens are created instead. + //{1}{W/P}{W/P}, Sacrifice two other artifacts and/or creatures: Put an indestructible counter on Mondrak. + String mondrak = "Mondrak, Glory Dominus"; + + Ability ability = new SimpleActivatedAbility( + Zone.ALL, + new CreateTokenEffect(new TreasureToken(), 2), + new ManaCostsImpl<>("") + ); + ability.addEffect(new CreateTokenEffect(new FoodToken(), 2)); + + addCustomCardWithAbility("Token-maker", playerA, ability); + addCard(Zone.BATTLEFIELD, playerA, mondrak); + addCard(Zone.BATTLEFIELD, playerA, yasharn); + addCard(Zone.BATTLEFIELD, playerA, "Bear Cub", 2); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + + checkPlayableAbility("Can't activate Mondrak with creatures", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W/P}{W/P}, Sacrifice", false); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "create two"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + checkPlayableAbility("Can't activate Mondrak with creatures or artifacts", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W/P}{W/P}, Sacrifice", false); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Bear Cub", 2); + assertPermanentCount(playerA, "Treasure Token", 4); + assertPermanentCount(playerA, "Food Token", 4); + } + + @Test + public void canSacrificeTriggeredAbility() { + /* + Unscrupulous Contractor + {2}{B} + Creature — Human Assassin + When this creature enters, you may sacrifice a creature. When you do, target player draws two cards and loses 2 life. + Plot {2}{B} + 3/2 + */ + String contractor = "Unscrupulous Contractor"; + /* + Bear Cub + {1}{G} + Creature - Bear + 2/2 + */ + String cub = "Bear Cub"; + + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, yasharn); + addCard(Zone.HAND, playerA, contractor); + addCard(Zone.HAND, playerB, contractor); + addCard(Zone.BATTLEFIELD, playerA, cub); + addCard(Zone.BATTLEFIELD, playerB, cub); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, contractor); + setChoice(playerA, true); + setChoice(playerA, cub); + addTarget(playerA, playerA); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, contractor); + setChoice(playerB, true); + setChoice(playerB, cub); + addTarget(playerB, playerB); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 2); + assertLife(playerA, 20 - 2); + assertGraveyardCount(playerA, cub, 1); + + assertHandCount(playerB, 1 + 2); //draw + contractor effect + assertLife(playerB, 20 - 2); + assertGraveyardCount(playerB, cub, 1); + } + + @Test + public void canPayLifeForTriggeredAbility() { + /* + Arrogant Poet + {1}{B} + Creature — Human Warlock + Whenever this creature attacks, you may pay 2 life. If you do, it gains flying until end of turn. + 2/1 + */ + String poet = "Arrogant Poet"; + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, poet); + addCard(Zone.BATTLEFIELD, playerA, yasharn); + + attack(1, playerA, poet); + setChoice(playerA, true); // pay 2 life + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 2); // combat damage + assertLife(playerA, 20 - 2); // paid life + assertAbility(playerA, poet, FlyingAbility.getInstance(), true); + } + + @Test + public void canSacWithGrist() { + /* + Grist, the Hunger Tide + {1}{B}{G} + Legendary Planeswalker — Grist + As long as Grist isn’t on the battlefield, it’s a 1/1 Insect creature in addition to its other types. + +1: Create a 1/1 black and green Insect creature token, then mill a card. If an Insect card was milled this way, put a loyalty counter on Grist and repeat this process. + −2: You may sacrifice a creature. When you do, destroy target creature or planeswalker. + −5: Each opponent loses life equal to the number of creature cards in your graveyard. + Loyalty: 3 + */ + String grist = "Grist, the Hunger Tide"; + /* + Bear Cub + {1}{G} + Creature - Bear + 2/2 + */ + String cub = "Bear Cub"; + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, grist); + addCard(Zone.BATTLEFIELD, playerA, yasharn); + addCard(Zone.BATTLEFIELD, playerA, cub); + addCard(Zone.BATTLEFIELD, playerB, grist + "@gristB"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-2:"); + setChoice(playerA, true); + setChoice(playerA, cub); + addTarget(playerA, "@gristB"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertCounterCount(grist, CounterType.LOYALTY, 1); + assertGraveyardCount(playerB, grist, 1); + assertGraveyardCount(playerA, cub, 1); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index ecc88c66b35..b60def4c78a 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 @@ -3760,13 +3760,13 @@ public class TestPlayer implements Player { } @Override - public PayLifeCostLevel getPayLifeCostLevel() { - return computerPlayer.getPayLifeCostLevel(); + public EnumSet getPayLifeCostRestrictions() { + return computerPlayer.getPayLifeCostRestrictions(); } @Override - public void setPayLifeCostLevel(PayLifeCostLevel payLifeCostLevel) { - computerPlayer.setPayLifeCostLevel(payLifeCostLevel); + public void addPayLifeCostRestriction(PayLifeCostRestriction payLifeCostRestriction) { + computerPlayer.addPayLifeCostRestriction(payLifeCostRestriction); } @Override @@ -3774,16 +3774,6 @@ public class TestPlayer implements Player { return computerPlayer.canPaySacrificeCost(permanent, source, controllerId, game); } - @Override - public FilterPermanent getSacrificeCostFilter() { - return computerPlayer.getSacrificeCostFilter(); - } - - @Override - public void setCanPaySacrificeCostFilter(FilterPermanent permanent) { - computerPlayer.setCanPaySacrificeCostFilter(permanent); - } - @Override public boolean canLoseByZeroOrLessLife() { return computerPlayer.canLoseByZeroOrLessLife(); diff --git a/Mage/src/main/java/mage/abilities/common/CantPayLifeOrSacrificeAbility.java b/Mage/src/main/java/mage/abilities/common/CantPayLifeOrSacrificeAbility.java new file mode 100644 index 00000000000..23e4db0cb93 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/CantPayLifeOrSacrificeAbility.java @@ -0,0 +1,151 @@ +package mage.abilities.common; + +import mage.abilities.Ability; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; + +import java.util.Optional; +import java.util.UUID; + +/** + * Effect used to prevent paying life and, optionally, sacrificing permanents as a cost for activated abilities and casting spells. + * @author Jmlundeen + */ +public class CantPayLifeOrSacrificeAbility extends SimpleStaticAbility { + + private final String rule; + + public CantPayLifeOrSacrificeAbility(FilterPermanent sacrificeFilter) { + this(false, sacrificeFilter); + } + + /** + * @param onlyNonManaAbilities boolean to set if the restriction should only apply to non-mana abilities + * @param sacrificeFilter filter for types of permanents that cannot be sacrificed, can be null if sacrifice not needed. + * e.g. Karn's Sylex + */ + public CantPayLifeOrSacrificeAbility(boolean onlyNonManaAbilities, FilterPermanent sacrificeFilter) { + super(new CantPayLifeEffect(onlyNonManaAbilities)); + if (sacrificeFilter != null) { + addEffect(new CantSacrificeEffect(onlyNonManaAbilities, sacrificeFilter)); + } + this.rule = makeRule(onlyNonManaAbilities, sacrificeFilter); + } + + private CantPayLifeOrSacrificeAbility(CantPayLifeOrSacrificeAbility effect) { + super(effect); + this.rule = effect.rule; + } + + public CantPayLifeOrSacrificeAbility copy() { + return new CantPayLifeOrSacrificeAbility(this); + } + + String makeRule(boolean nonManaAbilities, FilterPermanent sacrificeFilter) { + StringBuilder sb = new StringBuilder("Players can't pay life"); + if (sacrificeFilter != null) { + sb.append(" or sacrifice ").append(sacrificeFilter.getMessage()); + } + sb.append(" to cast spells or activate abilities"); + if (nonManaAbilities) { + sb.append(" that aren't mana abilities"); + } + sb.append("."); + return sb.toString(); + } + + @Override + public String getRule() { + return rule; + } +} + +class CantPayLifeEffect extends ContinuousEffectImpl { + + private final boolean onlyNonManaAbilities; + + /** + * @param onlyNonManaAbilities boolean to set if the restriction should only apply to non-mana abilities + */ + CantPayLifeEffect(boolean onlyNonManaAbilities) { + super(Duration.WhileOnBattlefield, Layer.PlayerEffects, SubLayer.NA, Outcome.Detriment); + this.onlyNonManaAbilities = onlyNonManaAbilities; + } + + private CantPayLifeEffect(CantPayLifeEffect effect) { + super(effect); + this.onlyNonManaAbilities = effect.onlyNonManaAbilities; + } + + public CantPayLifeEffect copy() { + return new CantPayLifeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { + Player player = game.getPlayer(playerId); + if (player == null) { + return false; + } + player.addPayLifeCostRestriction(Player.PayLifeCostRestriction.CAST_SPELLS); + if (this.onlyNonManaAbilities) { + player.addPayLifeCostRestriction(Player.PayLifeCostRestriction.ACTIVATE_NON_MANA_ABILITIES); + } else { + player.addPayLifeCostRestriction(Player.PayLifeCostRestriction.ACTIVATE_MANA_ABILITIES); + player.addPayLifeCostRestriction(Player.PayLifeCostRestriction.ACTIVATE_NON_MANA_ABILITIES); + } + } + return true; + } +} + +class CantSacrificeEffect extends ContinuousRuleModifyingEffectImpl { + + private final FilterPermanent sacrificeFilter; + private final boolean onlyNonManaAbilities; + + CantSacrificeEffect(boolean onlyNonManaAbilities, FilterPermanent sacrificeFilter) { + super(Duration.WhileOnBattlefield, Outcome.Detriment); + this.sacrificeFilter = sacrificeFilter; + this.onlyNonManaAbilities = onlyNonManaAbilities; + } + + private CantSacrificeEffect(CantSacrificeEffect effect) { + super(effect); + this.sacrificeFilter = effect.sacrificeFilter.copy(); + this.onlyNonManaAbilities = effect.onlyNonManaAbilities; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.PAY_SACRIFICE_COST; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Permanent permanent = game.getPermanent(event.getTargetId()); + Optional abilityOptional = game.getAbility(UUID.fromString(event.getData()), event.getSourceId()); + if (permanent == null || !abilityOptional.isPresent()) { + return false; + } + Ability abilityWithCost = abilityOptional.get(); + boolean isActivatedAbility = (onlyNonManaAbilities && abilityWithCost.isManaActivatedAbility()) || + (!onlyNonManaAbilities && abilityWithCost.isActivatedAbility()); + if (!isActivatedAbility && abilityWithCost.getAbilityType() != AbilityType.SPELL) { + return false; + } + return this.sacrificeFilter.match(permanent, event.getPlayerId(), source, game); + } + + @Override + public CantSacrificeEffect copy() { + return new CantSacrificeEffect(this); + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 21ad27b5d95..4e30e1a0c5a 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -688,6 +688,13 @@ public class GameEvent implements Serializable { /* rad counter life loss/gain effect */ RADIATION_GAIN_LIFE, + /* for checking sacrifice as a cost + targetId the permanent to be sacrificed + sourceId of the ability + playerId controller of ability + data id of the ability being paid for + */ + PAY_SACRIFICE_COST, // custom events - must store some unique data to track CUSTOM_EVENT; diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index bf3c4bba966..084919b8f6d 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -20,7 +20,6 @@ import mage.designations.Designation; import mage.designations.DesignationType; import mage.filter.FilterCard; import mage.filter.FilterMana; -import mage.filter.FilterPermanent; import mage.game.*; import mage.game.draft.Draft; import mage.game.events.GameEvent; @@ -37,10 +36,7 @@ import mage.util.Copyable; import mage.util.MultiAmountMessage; import java.io.Serializable; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; /** @@ -57,16 +53,13 @@ import java.util.stream.Collectors; public interface Player extends MageItem, Copyable { /** - * Enum used to indicate what each player is allowed to spend life on. - * By default it is set to `allAbilities`, but can be changed by effects. - * E.g. Angel of Jubilation sets it to `nonSpellnonActivatedAbilities`, - * and Karn's Sylex sets it to `onlyManaAbilities`. - *

- *

- * Default is PayLifeCostLevel.allAbilities. + * Enum used to indicate what each player is not allowed to spend life on. + * By default a player has no restrictions, but can be changed by effects. + * E.g. Angel of Jubilation adds `CAST_SPELLS` and 'ACTIVATE_ABILITIES', + * and Karn's Sylex adds `CAST_SPELLS` and 'ACTIVATE_NON_MANA_ABILITIES'. */ - enum PayLifeCostLevel { - allAbilities, nonSpellnonActivatedAbilities, onlyManaAbilities, none + enum PayLifeCostRestriction { + CAST_SPELLS, ACTIVATE_NON_MANA_ABILITIES, ACTIVATE_MANA_ABILITIES } /** @@ -177,14 +170,14 @@ public interface Player extends MageItem, Copyable { boolean isCanGainLife(); /** - * Is the player allowed to pay life for casting spells or activate activated abilities + * Adds a {@link PayLifeCostRestriction} to the set of restrictions. * - * @param payLifeCostLevel + * @param payLifeCostRestriction */ - void setPayLifeCostLevel(PayLifeCostLevel payLifeCostLevel); + void addPayLifeCostRestriction(PayLifeCostRestriction payLifeCostRestriction); - PayLifeCostLevel getPayLifeCostLevel(); + EnumSet getPayLifeCostRestrictions(); /** * Can the player pay life to cast or activate the given ability @@ -194,10 +187,6 @@ public interface Player extends MageItem, Copyable { */ boolean canPayLifeCost(Ability Ability); - void setCanPaySacrificeCostFilter(FilterPermanent filter); - - FilterPermanent getSacrificeCostFilter(); - boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game); void setLifeTotalCanChange(boolean lifeTotalCanChange); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 3ea064bccf2..ef0b43ea920 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -30,7 +30,6 @@ import mage.designations.DesignationType; import mage.designations.Speed; import mage.filter.FilterCard; import mage.filter.FilterMana; -import mage.filter.FilterPermanent; import mage.filter.StaticFilters; import mage.filter.common.FilterControlledPermanent; import mage.filter.common.FilterCreatureForCombat; @@ -150,14 +149,13 @@ public abstract class PlayerImpl implements Player, Serializable { protected boolean isFastFailInTestMode = true; protected boolean canGainLife = true; protected boolean canLoseLife = true; - protected PayLifeCostLevel payLifeCostLevel = PayLifeCostLevel.allAbilities; + protected EnumSet payLifeCostRestrictions = EnumSet.noneOf(PayLifeCostRestriction.class); protected boolean loseByZeroOrLessLife = true; protected boolean canPlotFromTopOfLibrary = false; protected boolean drawsFromBottom = false; protected boolean drawsOnOpponentsTurn = false; protected int speed = 0; - protected FilterPermanent sacrificeCostFilter; protected List alternativeSourceCosts = new ArrayList<>(); // TODO: rework turn controller to use single list (see other todos) @@ -264,8 +262,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.userData = player.userData; this.matchPlayer = player.matchPlayer; - this.payLifeCostLevel = player.payLifeCostLevel; - this.sacrificeCostFilter = player.sacrificeCostFilter; + this.payLifeCostRestrictions = player.payLifeCostRestrictions; this.alternativeSourceCosts = CardUtil.deepCopyObject(player.alternativeSourceCosts); this.storedBookmark = player.storedBookmark; @@ -364,9 +361,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.inRange.clear(); this.inRange.addAll(((PlayerImpl) player).inRange); - this.payLifeCostLevel = player.getPayLifeCostLevel(); - this.sacrificeCostFilter = player.getSacrificeCostFilter() != null - ? player.getSacrificeCostFilter().copy() : null; + this.payLifeCostRestrictions = player.getPayLifeCostRestrictions(); this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); this.canPlotFromTopOfLibrary = player.canPlotFromTopOfLibrary(); this.drawsFromBottom = player.isDrawsFromBottom(); @@ -481,14 +476,13 @@ public abstract class PlayerImpl implements Player, Serializable { //this.isTestMode // must keep this.canGainLife = true; this.canLoseLife = true; - this.payLifeCostLevel = PayLifeCostLevel.allAbilities; + this.payLifeCostRestrictions.clear(); this.loseByZeroOrLessLife = true; this.canPlotFromTopOfLibrary = false; this.drawsFromBottom = false; this.drawsOnOpponentsTurn = false; this.speed = 0; - this.sacrificeCostFilter = null; this.alternativeSourceCosts.clear(); this.isGameUnderControl = true; @@ -524,8 +518,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.maxAttackedBy = Integer.MAX_VALUE; this.canGainLife = true; this.canLoseLife = true; - this.payLifeCostLevel = PayLifeCostLevel.allAbilities; - this.sacrificeCostFilter = null; + this.payLifeCostRestrictions.clear(); this.loseByZeroOrLessLife = true; this.canPlotFromTopOfLibrary = false; this.drawsFromBottom = false; @@ -4683,46 +4676,41 @@ public abstract class PlayerImpl implements Player, Serializable { return false; } - switch (payLifeCostLevel) { - case allAbilities: - return true; - case onlyManaAbilities: - return ability.isManaAbility(); - case nonSpellnonActivatedAbilities: - return !ability.getAbilityType().isActivatedAbility() - && ability.getAbilityType() != AbilityType.SPELL; - case none: - default: - return false; + boolean canPay = true; + for (PayLifeCostRestriction restriction : payLifeCostRestrictions) { + switch (restriction) { + case CAST_SPELLS: + canPay &= ability.getAbilityType() != AbilityType.SPELL; + break; + case ACTIVATE_NON_MANA_ABILITIES: + canPay &= !ability.isNonManaActivatedAbility(); + break; + case ACTIVATE_MANA_ABILITIES: + canPay &= !ability.isManaActivatedAbility(); + break; + } } + return canPay; } @Override - public PayLifeCostLevel getPayLifeCostLevel() { - return payLifeCostLevel; + public EnumSet getPayLifeCostRestrictions() { + return payLifeCostRestrictions; } @Override - public void setPayLifeCostLevel(PayLifeCostLevel payLifeCostLevel) { - this.payLifeCostLevel = payLifeCostLevel; + public void addPayLifeCostRestriction(PayLifeCostRestriction payLifeCostRestriction) { + this.payLifeCostRestrictions.add(payLifeCostRestriction); } @Override public boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game) { - return permanent.canBeSacrificed() && - (sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, controllerId, source, game)); - } - - @Override - public void setCanPaySacrificeCostFilter(FilterPermanent filter - ) { - this.sacrificeCostFilter = filter; - } - - @Override - public FilterPermanent getSacrificeCostFilter() { - return sacrificeCostFilter; + if (!permanent.canBeSacrificed()) { + return false; + } + String sourceIdString = source.getId().toString(); + return !(game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PAY_SACRIFICE_COST, permanent.getId(), source, controllerId, sourceIdString, 1))); } @Override