From ffa837ae9532d60516533376106eb879d09a402c Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Wed, 29 Jul 2020 14:48:14 +0200 Subject: [PATCH] * Angel of Jubilation - Fixed that it did not only prevent life payment from casting spells or activating abilities (fixes #3663). --- .../src/mage/cards/g/GarzasAssassin.java | 2 +- .../src/mage/cards/i/InfernalDarkness.java | 4 +- Mage.Sets/src/mage/cards/l/LurkingEvil.java | 2 +- .../src/mage/cards/m/MurderousBetrayal.java | 2 +- Mage.Sets/src/mage/cards/s/SylvanLibrary.java | 2 +- Mage.Sets/src/mage/cards/w/WandOfDenial.java | 2 +- Mage.Sets/src/mage/cards/z/ZursWeirding.java | 2 +- .../continuous/AngelOfJubilationTest.java | 67 +++++++++++++++++++ .../java/org/mage/test/player/TestPlayer.java | 4 +- .../java/org/mage/test/stub/PlayerStub.java | 2 +- .../abilities/costs/common/PayLifeCost.java | 2 +- .../costs/common/PayVariableLifeCost.java | 2 +- Mage/src/main/java/mage/players/Player.java | 8 ++- .../main/java/mage/players/PlayerImpl.java | 57 +++++++++------- 14 files changed, 118 insertions(+), 40 deletions(-) diff --git a/Mage.Sets/src/mage/cards/g/GarzasAssassin.java b/Mage.Sets/src/mage/cards/g/GarzasAssassin.java index 475fc26f039..3136ddf356f 100644 --- a/Mage.Sets/src/mage/cards/g/GarzasAssassin.java +++ b/Mage.Sets/src/mage/cards/g/GarzasAssassin.java @@ -74,7 +74,7 @@ class GarzasAssassinCost extends CostImpl { @Override public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) { Player controller = game.getPlayer(controllerId); - return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost()); + return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost(ability)); } @Override diff --git a/Mage.Sets/src/mage/cards/i/InfernalDarkness.java b/Mage.Sets/src/mage/cards/i/InfernalDarkness.java index cef74faccc2..eae4f1c6dee 100644 --- a/Mage.Sets/src/mage/cards/i/InfernalDarkness.java +++ b/Mage.Sets/src/mage/cards/i/InfernalDarkness.java @@ -78,7 +78,7 @@ class InfernalDarknessCost extends CostImpl { manaCost.clearPaid(); if (manaCost.pay(ability, game, player.getId(), player.getId(), false) - && player.canPayLifeCost() + && player.canPayLifeCost(ability) && player.getLife() >= 1 && lifeCost.pay(ability, game, player.getId(), player.getId(), false)) { paid = true; @@ -91,7 +91,7 @@ class InfernalDarknessCost extends CostImpl { public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) { Player player = game.getPlayer(controllerId); if (player != null - && player.canPayLifeCost() + && player.canPayLifeCost(ability) && player.getLife() >= 1) { return true; } diff --git a/Mage.Sets/src/mage/cards/l/LurkingEvil.java b/Mage.Sets/src/mage/cards/l/LurkingEvil.java index c7ad56127a0..ef773aee93a 100644 --- a/Mage.Sets/src/mage/cards/l/LurkingEvil.java +++ b/Mage.Sets/src/mage/cards/l/LurkingEvil.java @@ -59,7 +59,7 @@ class LurkingEvilCost extends CostImpl { @Override public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) { Player controller = game.getPlayer(controllerId); - return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost()); + return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost(ability)); } @Override diff --git a/Mage.Sets/src/mage/cards/m/MurderousBetrayal.java b/Mage.Sets/src/mage/cards/m/MurderousBetrayal.java index 51cba4f02d7..17388b1ab66 100644 --- a/Mage.Sets/src/mage/cards/m/MurderousBetrayal.java +++ b/Mage.Sets/src/mage/cards/m/MurderousBetrayal.java @@ -67,7 +67,7 @@ class MurderousBetrayalCost extends CostImpl { @Override public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) { Player controller = game.getPlayer(controllerId); - return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost()); + return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost(ability)); } @Override diff --git a/Mage.Sets/src/mage/cards/s/SylvanLibrary.java b/Mage.Sets/src/mage/cards/s/SylvanLibrary.java index 18b1f1cfd3c..223e5306ccd 100644 --- a/Mage.Sets/src/mage/cards/s/SylvanLibrary.java +++ b/Mage.Sets/src/mage/cards/s/SylvanLibrary.java @@ -95,7 +95,7 @@ class SylvanLibraryEffect extends OneShotEffect { for (UUID cardId : target.getTargets()) { Card card = cards.get(cardId, game); if (card != null) { - if (controller.canPayLifeCost() + if (controller.canPayLifeCost(source) && controller.getLife() >= 4 && controller.chooseUse(outcome, "Pay 4 life for " + card.getLogName() + "? (Otherwise it's put on top of your library)", source, game)) { controller.loseLife(4, game, false); diff --git a/Mage.Sets/src/mage/cards/w/WandOfDenial.java b/Mage.Sets/src/mage/cards/w/WandOfDenial.java index 5137e4b110b..a30a6cd70bf 100644 --- a/Mage.Sets/src/mage/cards/w/WandOfDenial.java +++ b/Mage.Sets/src/mage/cards/w/WandOfDenial.java @@ -69,7 +69,7 @@ class WandOfDenialEffect extends OneShotEffect { MageObject sourceObject = game.getObject(source.getSourceId()); controller.lookAtCards(sourceObject != null ? sourceObject.getName() : "", new CardsImpl(card), game); if (!card.isLand() - && controller.canPayLifeCost() + && controller.canPayLifeCost(source) && controller.getLife() >= 2 && controller.chooseUse(Outcome.Neutral, "Pay 2 life to put " + card.getLogName() + " into graveyard?", source, game)) { controller.loseLife(2, game, false); diff --git a/Mage.Sets/src/mage/cards/z/ZursWeirding.java b/Mage.Sets/src/mage/cards/z/ZursWeirding.java index df49f02d4c5..ea6fe4bbd8f 100644 --- a/Mage.Sets/src/mage/cards/z/ZursWeirding.java +++ b/Mage.Sets/src/mage/cards/z/ZursWeirding.java @@ -98,7 +98,7 @@ class ZursWeirdingReplacementEffect extends ReplacementEffectImpl { continue; } Player otherPlayer = game.getPlayer(playerId); - if (otherPlayer.canPayLifeCost() + if (otherPlayer.canPayLifeCost(source) && otherPlayer.getLife() >= 2) { PayLifeCost lifeCost = new PayLifeCost(2); while (otherPlayer.canRespond() 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 1c0d53309b0..01aa07c6b96 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 @@ -177,4 +177,71 @@ public class AngelOfJubilationTest extends CardTestPlayerBase { assertPermanentCount(playerB, "Wasteland", 0); } + /** + * https://github.com/magefree/mage/issues/3663 + * + * Angel of Jubilation should just prevent paying life for activating + * abilities, but currently when it is out the opponent is not prompted to + * choose whether or not to pay life for Athreos. + */ + @Test + public void testAthreosLifePayNotPrevented() { + 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, "Silvercoat Lion"); + + // Indestructible + // As long as your devotion to white and black is less than seven, Athreos isn't a creature. + // Whenever another creature you own dies, return it to your hand unless target opponent pays 3 life. + addCard(Zone.BATTLEFIELD, playerA, "Athreos, God of Passage"); + + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + addCard(Zone.HAND, playerB, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion"); + setChoice(playerB, "Yes"); // Pay 3 life to prevent that returns to PlayerA's hand? + + addTarget(playerA, playerB); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerB, "Lightning Bolt", 1); + assertGraveyardCount(playerA, "Silvercoat Lion", 1); + + assertLife(playerA, 20); + assertLife(playerB, 17); + + } + + /** + * 5/1/2012 + * + * If a spell or activated ability has a cost that requires a player to pay + * life (as Griselbrand’s activated ability does) or sacrifice a creature + * (as Fling does), that spell or ability can’t be cast or activated. + */ + + @Test + public void testGriselbrandCantPay() { + 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"); + + // Pay 7 life: Draw seven cards. + addCard(Zone.BATTLEFIELD, playerB, "Griselbrand"); + + checkPlayableAbility("activated ability", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Pay 7 life", false); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + + } } 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 6538937e252..fd09f3522e4 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 @@ -3362,8 +3362,8 @@ public class TestPlayer implements Player { } @Override - public boolean canPayLifeCost() { - return computerPlayer.canPayLifeCost(); + public boolean canPayLifeCost(Ability ability) { + return computerPlayer.canPayLifeCost(ability); } @Override diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index a2d170a1e61..239a240ee81 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -181,7 +181,7 @@ public class PlayerStub implements Player { } @Override - public boolean canPayLifeCost() { + public boolean canPayLifeCost(Ability ability) { return false; } diff --git a/Mage/src/main/java/mage/abilities/costs/common/PayLifeCost.java b/Mage/src/main/java/mage/abilities/costs/common/PayLifeCost.java index 18c902d5af4..79f68bb0c68 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/PayLifeCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/PayLifeCost.java @@ -40,7 +40,7 @@ public class PayLifeCost extends CostImpl { //life total; in other words, the player loses that much life. (Players can always pay 0 life.) int lifeToPayAmount = amount.calculate(game, ability, null); // Paying 0 life is not considered paying any life. - if (lifeToPayAmount > 0 && !game.getPlayer(controllerId).canPayLifeCost()) { + if (lifeToPayAmount > 0 && !game.getPlayer(controllerId).canPayLifeCost(ability)) { return false; } return game.getPlayer(controllerId).getLife() >= lifeToPayAmount || lifeToPayAmount == 0; diff --git a/Mage/src/main/java/mage/abilities/costs/common/PayVariableLifeCost.java b/Mage/src/main/java/mage/abilities/costs/common/PayVariableLifeCost.java index 4da6753e7e0..6e69eead369 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/PayVariableLifeCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/PayVariableLifeCost.java @@ -42,7 +42,7 @@ public class PayVariableLifeCost extends VariableCostImpl { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { // Paying 0 life is not considered paying any life, so paying 0 is still allowed - if (game.getPlayer(source.getControllerId()).canPayLifeCost()) { + if (game.getPlayer(source.getControllerId()).canPayLifeCost(source)) { maxValue = controller.getLife(); } } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 9e4478acb48..df36f3469cb 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -103,7 +103,13 @@ public interface Player extends MageItem, Copyable { void setCanPayLifeCost(boolean canPayLifeCost); - boolean canPayLifeCost(); + /** + * Can the player pay life for spells or activated abilities + * + * @param Ability + * @return + */ + boolean canPayLifeCost(Ability Ability); void setCanPaySacrificeCostFilter(FilterPermanent filter); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 22fca1dfb83..d12a0c3e918 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -332,7 +332,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.inRange.clear(); this.inRange.addAll(player.getInRange()); - this.canPayLifeCost = player.canPayLifeCost(); + this.canPayLifeCost = player.canPayLifeCost(null); this.sacrificeCostFilter = player.getSacrificeCostFilter() != null ? player.getSacrificeCostFilter().copy() : null; this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); @@ -1859,9 +1859,9 @@ public abstract class PlayerImpl implements Player, Serializable { } private List getPermanentsThatCanBeUntapped(Game game, - List canBeUntapped, - RestrictionUntapNotMoreThanEffect handledEffect, - Map>, Integer> notMoreThanEffectsUsage) { + List canBeUntapped, + RestrictionUntapNotMoreThanEffect handledEffect, + Map>, Integer> notMoreThanEffectsUsage) { List leftForUntap = new ArrayList<>(); // select permanents that can still be untapped for (Permanent permanent : canBeUntapped) { @@ -2574,7 +2574,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId, - boolean triggerEvents) { + boolean triggerEvents) { //20091005 - 701.14c Library searchedLibrary = null; String searchInfo = null; @@ -2822,7 +2822,7 @@ public abstract class PlayerImpl implements Player, Serializable { */ @Override public PlanarDieRoll rollPlanarDie(Game game, List appliedEffects, int numberChaosSides, - int numberPlanarSides) { + int numberPlanarSides) { int result = RandomUtil.nextInt(9) + 1; PlanarDieRoll roll = PlanarDieRoll.NIL_ROLL; if (numberChaosSides + numberPlanarSides > 9) { @@ -3757,8 +3757,13 @@ public abstract class PlayerImpl implements Player, Serializable { } @Override - public boolean canPayLifeCost() { - return isLifeTotalCanChange() && canPayLifeCost; + public boolean canPayLifeCost(Ability ability) { + if (!canPayLifeCost + && (AbilityType.ACTIVATED.equals(ability.getAbilityType()) + || AbilityType.SPELL.equals(ability.getAbilityType()))) { + return false; + } + return isLifeTotalCanChange(); } @Override @@ -3769,7 +3774,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean canPaySacrificeCost(Permanent permanent, UUID sourceId, - UUID controllerId, Game game + UUID controllerId, Game game ) { return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, sourceId, controllerId, game); } @@ -3922,8 +3927,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCards(Card card, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { Set cardList = new HashSet<>(); if (card != null) { @@ -3934,22 +3939,22 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCards(Cards cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return moveCards(cards.getCards(game), toZone, source, game); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return moveCards(cards, toZone, source, game, false, false, false, null); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { if (cards.isEmpty()) { return true; @@ -4051,8 +4056,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardsToExile(Card card, Ability source, - Game game, boolean withName, UUID exileId, - String exileZoneName + Game game, boolean withName, UUID exileId, + String exileZoneName ) { Set cards = new HashSet<>(); cards.add(card); @@ -4061,8 +4066,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardsToExile(Set cards, Ability source, - Game game, boolean withName, UUID exileId, - String exileZoneName + Game game, boolean withName, UUID exileId, + String exileZoneName ) { if (cards.isEmpty()) { return true; @@ -4078,14 +4083,14 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToHandWithInfo(Card card, UUID sourceId, - Game game + Game game ) { return this.moveCardToHandWithInfo(card, sourceId, game, true); } @Override public boolean moveCardToHandWithInfo(Card card, UUID sourceId, - Game game, boolean withName + Game game, boolean withName ) { boolean result = false; Zone fromZone = game.getState().getZone(card.getId()); @@ -4110,7 +4115,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public Set moveCardsToGraveyardWithInfo(Set allCards, Ability source, - Game game, Zone fromZone + Game game, Zone fromZone ) { UUID sourceId = source == null ? null : source.getSourceId(); Set movedCards = new LinkedHashSet<>(); @@ -4181,7 +4186,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToGraveyardWithInfo(Card card, UUID sourceId, - Game game, Zone fromZone + Game game, Zone fromZone ) { if (card == null) { return false; @@ -4210,8 +4215,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToLibraryWithInfo(Card card, UUID sourceId, - Game game, Zone fromZone, - boolean toTop, boolean withName + Game game, Zone fromZone, + boolean toTop, boolean withName ) { if (card == null) { return false; @@ -4276,7 +4281,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, UUID sourceId, - Game game, Zone fromZone, boolean withName) { + Game game, Zone fromZone, boolean withName) { if (card == null) { return false; }