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
This commit is contained in:
Jmlundeen 2025-08-09 17:53:43 -05:00 committed by GitHub
parent 2833460e59
commit 574d7f91a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 610 additions and 387 deletions

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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 cant pay life to cast spells or to activate abilities that arent mana abilities.
this.addAbility(new SimpleStaticAbility(new KarnsSylexEffect()));
this.addAbility(new CantPayLifeOrSacrificeAbility(true, null));
// {X}, {T}, Exile Karns 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() {

View file

@ -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<SubType, BasicManaAbility> abilityMap = new HashMap<SubType, BasicManaAbility>() {{
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<Ability> 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;
}
}

View file

@ -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> 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;
}
}

View file

@ -337,4 +337,20 @@ public class BargainTest extends CardTestPlayerBase {
assertLife(playerA, 20 + 3);
assertTappedCount("Forest", true, 7);
}
@Test
public void testCantBargainWithRestriction() {
setStrictChooseMode(true);
// Players cant 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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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 isnt on the battlefield, its 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);
}
}

View file

@ -3760,13 +3760,13 @@ public class TestPlayer implements Player {
}
@Override
public PayLifeCostLevel getPayLifeCostLevel() {
return computerPlayer.getPayLifeCostLevel();
public EnumSet<PayLifeCostRestriction> 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();

View file

@ -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<Ability> 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);
}
}

View file

@ -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;

View file

@ -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<Player> {
/**
* 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`.
* <p>
* <p>
* 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<Player> {
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<PayLifeCostRestriction> getPayLifeCostRestrictions();
/**
* Can the player pay life to cast or activate the given ability
@ -194,10 +187,6 @@ public interface Player extends MageItem, Copyable<Player> {
*/
boolean canPayLifeCost(Ability Ability);
void setCanPaySacrificeCostFilter(FilterPermanent filter);
FilterPermanent getSacrificeCostFilter();
boolean canPaySacrificeCost(Permanent permanent, Ability source, UUID controllerId, Game game);
void setLifeTotalCanChange(boolean lifeTotalCanChange);

View file

@ -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<PayLifeCostRestriction> 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> 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<PayLifeCostRestriction> 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));
if (!permanent.canBeSacrificed()) {
return false;
}
@Override
public void setCanPaySacrificeCostFilter(FilterPermanent filter
) {
this.sacrificeCostFilter = filter;
}
@Override
public FilterPermanent getSacrificeCostFilter() {
return sacrificeCostFilter;
String sourceIdString = source.getId().toString();
return !(game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PAY_SACRIFICE_COST, permanent.getId(), source, controllerId, sourceIdString, 1)));
}
@Override