* Angel of Jubilation - Fixed that it did not only prevent life payment from casting spells or activating abilities (fixes #3663).

This commit is contained in:
LevelX2 2020-07-29 14:48:14 +02:00
parent 57de10d609
commit ffa837ae95
14 changed files with 118 additions and 40 deletions

View file

@ -74,7 +74,7 @@ class GarzasAssassinCost extends CostImpl {
@Override @Override
public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) { public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) {
Player controller = game.getPlayer(controllerId); Player controller = game.getPlayer(controllerId);
return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost()); return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost(ability));
} }
@Override @Override

View file

@ -78,7 +78,7 @@ class InfernalDarknessCost extends CostImpl {
manaCost.clearPaid(); manaCost.clearPaid();
if (manaCost.pay(ability, game, player.getId(), player.getId(), false) if (manaCost.pay(ability, game, player.getId(), player.getId(), false)
&& player.canPayLifeCost() && player.canPayLifeCost(ability)
&& player.getLife() >= 1 && player.getLife() >= 1
&& lifeCost.pay(ability, game, player.getId(), player.getId(), false)) { && lifeCost.pay(ability, game, player.getId(), player.getId(), false)) {
paid = true; paid = true;
@ -91,7 +91,7 @@ class InfernalDarknessCost extends CostImpl {
public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) { public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) {
Player player = game.getPlayer(controllerId); Player player = game.getPlayer(controllerId);
if (player != null if (player != null
&& player.canPayLifeCost() && player.canPayLifeCost(ability)
&& player.getLife() >= 1) { && player.getLife() >= 1) {
return true; return true;
} }

View file

@ -59,7 +59,7 @@ class LurkingEvilCost extends CostImpl {
@Override @Override
public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) { public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) {
Player controller = game.getPlayer(controllerId); Player controller = game.getPlayer(controllerId);
return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost()); return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost(ability));
} }
@Override @Override

View file

@ -67,7 +67,7 @@ class MurderousBetrayalCost extends CostImpl {
@Override @Override
public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) { public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) {
Player controller = game.getPlayer(controllerId); Player controller = game.getPlayer(controllerId);
return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost()); return controller != null && (controller.getLife() < 1 || controller.canPayLifeCost(ability));
} }
@Override @Override

View file

@ -95,7 +95,7 @@ class SylvanLibraryEffect extends OneShotEffect {
for (UUID cardId : target.getTargets()) { for (UUID cardId : target.getTargets()) {
Card card = cards.get(cardId, game); Card card = cards.get(cardId, game);
if (card != null) { if (card != null) {
if (controller.canPayLifeCost() if (controller.canPayLifeCost(source)
&& controller.getLife() >= 4 && controller.getLife() >= 4
&& controller.chooseUse(outcome, "Pay 4 life for " + card.getLogName() + "? (Otherwise it's put on top of your library)", source, game)) { && 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); controller.loseLife(4, game, false);

View file

@ -69,7 +69,7 @@ class WandOfDenialEffect extends OneShotEffect {
MageObject sourceObject = game.getObject(source.getSourceId()); MageObject sourceObject = game.getObject(source.getSourceId());
controller.lookAtCards(sourceObject != null ? sourceObject.getName() : "", new CardsImpl(card), game); controller.lookAtCards(sourceObject != null ? sourceObject.getName() : "", new CardsImpl(card), game);
if (!card.isLand() if (!card.isLand()
&& controller.canPayLifeCost() && controller.canPayLifeCost(source)
&& controller.getLife() >= 2 && controller.getLife() >= 2
&& controller.chooseUse(Outcome.Neutral, "Pay 2 life to put " + card.getLogName() + " into graveyard?", source, game)) { && controller.chooseUse(Outcome.Neutral, "Pay 2 life to put " + card.getLogName() + " into graveyard?", source, game)) {
controller.loseLife(2, game, false); controller.loseLife(2, game, false);

View file

@ -98,7 +98,7 @@ class ZursWeirdingReplacementEffect extends ReplacementEffectImpl {
continue; continue;
} }
Player otherPlayer = game.getPlayer(playerId); Player otherPlayer = game.getPlayer(playerId);
if (otherPlayer.canPayLifeCost() if (otherPlayer.canPayLifeCost(source)
&& otherPlayer.getLife() >= 2) { && otherPlayer.getLife() >= 2) {
PayLifeCost lifeCost = new PayLifeCost(2); PayLifeCost lifeCost = new PayLifeCost(2);
while (otherPlayer.canRespond() while (otherPlayer.canRespond()

View file

@ -177,4 +177,71 @@ public class AngelOfJubilationTest extends CardTestPlayerBase {
assertPermanentCount(playerB, "Wasteland", 0); 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 Griselbrands activated ability does) or sacrifice a creature
* (as Fling does), that spell or ability cant 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();
}
} }

View file

@ -3362,8 +3362,8 @@ public class TestPlayer implements Player {
} }
@Override @Override
public boolean canPayLifeCost() { public boolean canPayLifeCost(Ability ability) {
return computerPlayer.canPayLifeCost(); return computerPlayer.canPayLifeCost(ability);
} }
@Override @Override

View file

@ -181,7 +181,7 @@ public class PlayerStub implements Player {
} }
@Override @Override
public boolean canPayLifeCost() { public boolean canPayLifeCost(Ability ability) {
return false; return false;
} }

View file

@ -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.) //life total; in other words, the player loses that much life. (Players can always pay 0 life.)
int lifeToPayAmount = amount.calculate(game, ability, null); int lifeToPayAmount = amount.calculate(game, ability, null);
// Paying 0 life is not considered paying any life. // 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 false;
} }
return game.getPlayer(controllerId).getLife() >= lifeToPayAmount || lifeToPayAmount == 0; return game.getPlayer(controllerId).getLife() >= lifeToPayAmount || lifeToPayAmount == 0;

View file

@ -42,7 +42,7 @@ public class PayVariableLifeCost extends VariableCostImpl {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller != null) {
// Paying 0 life is not considered paying any life, so paying 0 is still allowed // 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(); maxValue = controller.getLife();
} }
} }

View file

@ -103,7 +103,13 @@ public interface Player extends MageItem, Copyable<Player> {
void setCanPayLifeCost(boolean canPayLifeCost); 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); void setCanPaySacrificeCostFilter(FilterPermanent filter);

View file

@ -332,7 +332,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.inRange.clear(); this.inRange.clear();
this.inRange.addAll(player.getInRange()); this.inRange.addAll(player.getInRange());
this.canPayLifeCost = player.canPayLifeCost(); this.canPayLifeCost = player.canPayLifeCost(null);
this.sacrificeCostFilter = player.getSacrificeCostFilter() != null this.sacrificeCostFilter = player.getSacrificeCostFilter() != null
? player.getSacrificeCostFilter().copy() : null; ? player.getSacrificeCostFilter().copy() : null;
this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife(); this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife();
@ -1859,9 +1859,9 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
private List<Permanent> getPermanentsThatCanBeUntapped(Game game, private List<Permanent> getPermanentsThatCanBeUntapped(Game game,
List<Permanent> canBeUntapped, List<Permanent> canBeUntapped,
RestrictionUntapNotMoreThanEffect handledEffect, RestrictionUntapNotMoreThanEffect handledEffect,
Map<Entry<RestrictionUntapNotMoreThanEffect, Set<Ability>>, Integer> notMoreThanEffectsUsage) { Map<Entry<RestrictionUntapNotMoreThanEffect, Set<Ability>>, Integer> notMoreThanEffectsUsage) {
List<Permanent> leftForUntap = new ArrayList<>(); List<Permanent> leftForUntap = new ArrayList<>();
// select permanents that can still be untapped // select permanents that can still be untapped
for (Permanent permanent : canBeUntapped) { for (Permanent permanent : canBeUntapped) {
@ -2574,7 +2574,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId, public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId,
boolean triggerEvents) { boolean triggerEvents) {
//20091005 - 701.14c //20091005 - 701.14c
Library searchedLibrary = null; Library searchedLibrary = null;
String searchInfo = null; String searchInfo = null;
@ -2822,7 +2822,7 @@ public abstract class PlayerImpl implements Player, Serializable {
*/ */
@Override @Override
public PlanarDieRoll rollPlanarDie(Game game, List<UUID> appliedEffects, int numberChaosSides, public PlanarDieRoll rollPlanarDie(Game game, List<UUID> appliedEffects, int numberChaosSides,
int numberPlanarSides) { int numberPlanarSides) {
int result = RandomUtil.nextInt(9) + 1; int result = RandomUtil.nextInt(9) + 1;
PlanarDieRoll roll = PlanarDieRoll.NIL_ROLL; PlanarDieRoll roll = PlanarDieRoll.NIL_ROLL;
if (numberChaosSides + numberPlanarSides > 9) { if (numberChaosSides + numberPlanarSides > 9) {
@ -3757,8 +3757,13 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean canPayLifeCost() { public boolean canPayLifeCost(Ability ability) {
return isLifeTotalCanChange() && canPayLifeCost; if (!canPayLifeCost
&& (AbilityType.ACTIVATED.equals(ability.getAbilityType())
|| AbilityType.SPELL.equals(ability.getAbilityType()))) {
return false;
}
return isLifeTotalCanChange();
} }
@Override @Override
@ -3769,7 +3774,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean canPaySacrificeCost(Permanent permanent, UUID sourceId, public boolean canPaySacrificeCost(Permanent permanent, UUID sourceId,
UUID controllerId, Game game UUID controllerId, Game game
) { ) {
return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, sourceId, controllerId, game); return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, sourceId, controllerId, game);
} }
@ -3922,8 +3927,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCards(Card card, Zone toZone, public boolean moveCards(Card card, Zone toZone,
Ability source, Game game, Ability source, Game game,
boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects
) { ) {
Set<Card> cardList = new HashSet<>(); Set<Card> cardList = new HashSet<>();
if (card != null) { if (card != null) {
@ -3934,22 +3939,22 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCards(Cards cards, Zone toZone, public boolean moveCards(Cards cards, Zone toZone,
Ability source, Game game Ability source, Game game
) { ) {
return moveCards(cards.getCards(game), toZone, source, game); return moveCards(cards.getCards(game), toZone, source, game);
} }
@Override @Override
public boolean moveCards(Set<Card> cards, Zone toZone, public boolean moveCards(Set<Card> cards, Zone toZone,
Ability source, Game game Ability source, Game game
) { ) {
return moveCards(cards, toZone, source, game, false, false, false, null); return moveCards(cards, toZone, source, game, false, false, false, null);
} }
@Override @Override
public boolean moveCards(Set<Card> cards, Zone toZone, public boolean moveCards(Set<Card> cards, Zone toZone,
Ability source, Game game, Ability source, Game game,
boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects
) { ) {
if (cards.isEmpty()) { if (cards.isEmpty()) {
return true; return true;
@ -4051,8 +4056,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardsToExile(Card card, Ability source, public boolean moveCardsToExile(Card card, Ability source,
Game game, boolean withName, UUID exileId, Game game, boolean withName, UUID exileId,
String exileZoneName String exileZoneName
) { ) {
Set<Card> cards = new HashSet<>(); Set<Card> cards = new HashSet<>();
cards.add(card); cards.add(card);
@ -4061,8 +4066,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardsToExile(Set<Card> cards, Ability source, public boolean moveCardsToExile(Set<Card> cards, Ability source,
Game game, boolean withName, UUID exileId, Game game, boolean withName, UUID exileId,
String exileZoneName String exileZoneName
) { ) {
if (cards.isEmpty()) { if (cards.isEmpty()) {
return true; return true;
@ -4078,14 +4083,14 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardToHandWithInfo(Card card, UUID sourceId, public boolean moveCardToHandWithInfo(Card card, UUID sourceId,
Game game Game game
) { ) {
return this.moveCardToHandWithInfo(card, sourceId, game, true); return this.moveCardToHandWithInfo(card, sourceId, game, true);
} }
@Override @Override
public boolean moveCardToHandWithInfo(Card card, UUID sourceId, public boolean moveCardToHandWithInfo(Card card, UUID sourceId,
Game game, boolean withName Game game, boolean withName
) { ) {
boolean result = false; boolean result = false;
Zone fromZone = game.getState().getZone(card.getId()); Zone fromZone = game.getState().getZone(card.getId());
@ -4110,7 +4115,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public Set<Card> moveCardsToGraveyardWithInfo(Set<Card> allCards, Ability source, public Set<Card> moveCardsToGraveyardWithInfo(Set<Card> allCards, Ability source,
Game game, Zone fromZone Game game, Zone fromZone
) { ) {
UUID sourceId = source == null ? null : source.getSourceId(); UUID sourceId = source == null ? null : source.getSourceId();
Set<Card> movedCards = new LinkedHashSet<>(); Set<Card> movedCards = new LinkedHashSet<>();
@ -4181,7 +4186,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardToGraveyardWithInfo(Card card, UUID sourceId, public boolean moveCardToGraveyardWithInfo(Card card, UUID sourceId,
Game game, Zone fromZone Game game, Zone fromZone
) { ) {
if (card == null) { if (card == null) {
return false; return false;
@ -4210,8 +4215,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardToLibraryWithInfo(Card card, UUID sourceId, public boolean moveCardToLibraryWithInfo(Card card, UUID sourceId,
Game game, Zone fromZone, Game game, Zone fromZone,
boolean toTop, boolean withName boolean toTop, boolean withName
) { ) {
if (card == null) { if (card == null) {
return false; return false;
@ -4276,7 +4281,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, UUID sourceId, 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) { if (card == null) {
return false; return false;
} }