mirror of
https://github.com/magefree/mage.git
synced 2026-01-10 04:42:07 -08:00
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:
parent
2833460e59
commit
574d7f91a5
18 changed files with 610 additions and 387 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
@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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue