[DMU] Karn's Sylex (#9507)

This commit is contained in:
Alex Vasile 2022-09-17 22:37:56 -04:00 committed by GitHub
parent adbebfdd3e
commit b14af42280
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 285 additions and 30 deletions

View file

@ -83,6 +83,9 @@ public class ComputerPlayer extends PlayerImpl implements Player {
private transient ManaCost currentUnpaidMana;
// For stopping infinite loops when trying to pay Phyrexian mana when the player can't spend life and no other sources are available
private transient boolean alreadyTryingToPayPhyrexian;
public ComputerPlayer(String name, RangeOfInfluence range) {
super(name, range);
human = false;
@ -1664,9 +1667,16 @@ public class ComputerPlayer extends PlayerImpl implements Player {
}
}
if (alreadyTryingToPayPhyrexian) {
return false;
}
// pay phyrexian life costs
if (cost.isPhyrexian()) {
return cost.pay(ability, game, ability, playerId, false, null) || approvingObject != null;
alreadyTryingToPayPhyrexian = true;
boolean paidPhyrexian = cost.pay(ability, game, ability, playerId, false, null) || approvingObject != null;
alreadyTryingToPayPhyrexian = false;
return paidPhyrexian;
}
// pay special mana like convoke cost (tap for pay)

View file

@ -75,7 +75,7 @@ class AngelOfJubilationEffect extends ContinuousEffectImpl {
public boolean apply(Game game, Ability source) {
for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) {
Player player = game.getPlayer(playerId);
player.setCanPayLifeCost(false);
player.setPayLifeCostLevel(Player.PayLifeCostLevel.nonSpellnonActivatedAbilities);
player.setCanPaySacrificeCostFilter(new FilterCreaturePermanent());
}
return true;

View file

@ -0,0 +1,107 @@
package mage.cards.k;
import mage.abilities.Ability;
import mage.abilities.common.ActivateAsSorceryActivatedAbility;
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.filter.common.FilterCreaturePermanent;
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 java.util.UUID;
/**
* @author Alex-Vasile
*/
public class KarnsSylex extends CardImpl {
public KarnsSylex(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{}, "");
// Karns Sylex
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()));
// {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}"));
ability.addCost(new TapSourceCost());
ability.addCost(new ExileSourceCost());
this.addAbility(ability);
}
private KarnsSylex(final KarnsSylex card) {
super(card);
}
@Override
public KarnsSylex copy() {
return new KarnsSylex(this);
}
}
class KarnsSylexEffect extends ContinuousEffectImpl {
public KarnsSylexEffect() {
super(Duration.WhileOnBattlefield, Layer.PlayerEffects, SubLayer.NA, Outcome.Detriment);
staticText = "Players can't pay life or sacrifice creatures to cast spells";
}
public 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);
player.setCanPaySacrificeCostFilter(new FilterCreaturePermanent());
}
return true;
}
}
class KarnsSylexDestroyEffect extends OneShotEffect {
KarnsSylexDestroyEffect() {
super(Outcome.DestroyPermanent);
staticText = "Destroy each nonland permanent with mana value X or less.";
}
private KarnsSylexDestroyEffect(final KarnsSylexDestroyEffect effect) {
super(effect);
}
public KarnsSylexDestroyEffect copy() {
return new KarnsSylexDestroyEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
FilterNonlandPermanent filter = new FilterNonlandPermanent();
filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, source.getManaCostsToPay().getX() + 1));
boolean destroyed = false;
for (Permanent permanent : game.getState().getBattlefield().getActivePermanents(filter, source.getControllerId(), game)) {
destroyed |= permanent.destroy(source, game);
}
return destroyed;
}
}

View file

@ -150,6 +150,7 @@ public final class DominariaUnited extends ExpansionSet {
cards.add(new SetCardInfo("Joint Exploration", 56, Rarity.UNCOMMON, mage.cards.j.JointExploration.class));
cards.add(new SetCardInfo("Juniper Order Rootweaver", 22, Rarity.COMMON, mage.cards.j.JuniperOrderRootweaver.class));
cards.add(new SetCardInfo("Karn, Living Legacy", 1, Rarity.MYTHIC, mage.cards.k.KarnLivingLegacy.class));
cards.add(new SetCardInfo("Karn's Sylex", 234, Rarity.MYTHIC, mage.cards.k.KarnsSylex.class));
cards.add(new SetCardInfo("Karplusan Forest", 250, Rarity.RARE, mage.cards.k.KarplusanForest.class));
cards.add(new SetCardInfo("Keldon Flamesage", 135, Rarity.RARE, mage.cards.k.KeldonFlamesage.class));
cards.add(new SetCardInfo("Keldon Strike Team", 136, Rarity.COMMON, mage.cards.k.KeldonStrikeTeam.class));

View file

@ -229,7 +229,6 @@ public class AngelOfJubilationTest extends CardTestPlayerBase {
* 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);
@ -244,6 +243,5 @@ public class AngelOfJubilationTest extends CardTestPlayerBase {
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
}
}

View file

@ -0,0 +1,79 @@
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;
/**
* {@link mage.cards.k.KarnsSylex Karn's Sylex}
* Karns Sylex enters the battlefield tapped.
* Players cant pay life to cast spells or to activate abilities that arent mana abilities.
* {X}, {T}, Exile Karns Sylex: Destroy each nonland permanent with mana value X or less. Activate only as a sorcery.
*
* @author Alex-Vasile
*/
public class KarnsSylexTest extends CardTestPlayerBase {
private static final String karnsSylex = "Karn's Sylex";
/**
* Test that it does not allow for Phyrexian mana to be paid with life.
*/
@Test
public void blockPhyrexianMana() {
// {3}{B/P}
String tezzeretsGambit = "Tezzeret's Gambit";
addCard(Zone.HAND, playerA, tezzeretsGambit);
addCard(Zone.BATTLEFIELD, playerA, karnsSylex);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
checkPlayableAbility("Can't pay life to cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + tezzeretsGambit, false);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertLife(playerA, 20);
}
/**
* Blocks things like Bolas's Citadel.
*/
@Test
public void blockBolassCitadel() {
// You may play lands and cast spells from the top of your library.
// If you cast a spell this way, pay life equal to its mana value rather than pay its mana cost.
String bolassCitadel = "Bolas's Citadel";
addCard(Zone.BATTLEFIELD, playerA, bolassCitadel);
addCard(Zone.BATTLEFIELD, playerA, karnsSylex);
addCard(Zone.LIBRARY, playerA, "Lightning Bolt");
skipInitShuffling();
checkPlayableAbility("Can't pay life to cast", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", false);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
}
/**
* Test that it works with mana abilities, e.g. Thran Portal.
*/
@Test
public void allowsManaAbilities() {
addCard(Zone.HAND, playerA, "Thran Portal");
addCard(Zone.HAND, playerA, "Lightning Bolt");
addCard(Zone.BATTLEFIELD, playerA, karnsSylex);
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thran Portal");
setChoice(playerA, "Thran");
setChoice(playerA, "Mountain");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", playerB);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerA, 20 - 1);
assertLife(playerB, 20 - 3);
assertGraveyardCount(playerA, "Lightning Bolt", 1);
}
}

View file

@ -3679,13 +3679,13 @@ public class TestPlayer implements Player {
}
@Override
public boolean getCanPayLifeCost() {
return computerPlayer.getCanPayLifeCost();
public PayLifeCostLevel getPayLifeCostLevel() {
return computerPlayer.getPayLifeCostLevel();
}
@Override
public void setCanPayLifeCost(boolean canPayLifeCost) {
computerPlayer.setCanPayLifeCost(canPayLifeCost);
public void setPayLifeCostLevel(PayLifeCostLevel payLifeCostLevel) {
computerPlayer.setPayLifeCostLevel(payLifeCostLevel);
}
@Override

View file

@ -183,13 +183,12 @@ public class PlayerStub implements Player {
}
@Override
public void setCanPayLifeCost(boolean canPayLifeCost) {
public void setPayLifeCostLevel(PayLifeCostLevel playLifeCostLevel) {
}
@Override
public boolean getCanPayLifeCost() {
return false;
public PayLifeCostLevel getPayLifeCostLevel() {
return PayLifeCostLevel.none;
}
@Override

View file

@ -8,12 +8,14 @@ import mage.abilities.costs.Costs;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.effects.Effect;
import mage.abilities.effects.Effects;
import mage.abilities.mana.ManaAbility;
import mage.abilities.mana.ManaOptions;
import mage.cards.Card;
import mage.constants.*;
import mage.game.Game;
import mage.game.command.CommandObject;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.UUID;
@ -204,7 +206,9 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
@Override
public ManaOptions getMinimumCostToActivate(UUID playerId, Game game) {
return getManaCostsToPay().getOptions();
Player player = game.getPlayer(playerId);
return getManaCostsToPay().getOptions(player.canPayLifeCost(this));
}
protected boolean controlsAbility(UUID playerId, Game game) {

View file

@ -31,6 +31,21 @@ public interface ManaCost extends Cost {
ManaOptions getOptions();
/**
* Return all options for paying the mana cost (this) while taking into accoutn if the player can pay life.
* Used to correctly highlight (or not) spells with Phyrexian mana depending on if the player can pay life costs.
* <p>
* E.g. Tezzeret's Gambit has a cost of {3}{U/P}.
* If `canPayLifeCost == true` the two options are {3}{U} and {3}. The second including a 2 life cost that is not
* captured by the ManaOptions object being returned.
* However, if `canPayLifeCost == false` than the return is only {3}{U} since the Phyrexian mana MUST be paid with
* a {U} and not with 2 life.
*
* @param canPayLifeCost if the player is able to pay life for the ability/effect that this cost is associated with
* @return
*/
ManaOptions getOptions(boolean canPayLifeCost);
boolean testPay(Mana testMana);
Filter getSourceFilter();

View file

@ -69,8 +69,19 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost {
@Override
public ManaOptions getOptions() {
return getOptions(true);
}
@Override
public ManaOptions getOptions(boolean canPayLifeCost) {
if (!canPayLifeCost && this.isPhyrexian()) {
ManaOptions optionsFiltered = new ManaOptions();
optionsFiltered.add(this.cost);
return optionsFiltered;
} else {
return options;
}
}
@Override
public void clearPaid() {

View file

@ -542,9 +542,14 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> implements M
@Override
public ManaOptions getOptions() {
return getOptions(true);
}
@Override
public ManaOptions getOptions(boolean canPayLifeCost) {
ManaOptions options = new ManaOptions();
for (ManaCost cost : this) {
options.addMana(cost.getOptions());
options.addMana(cost.getOptions(canPayLifeCost));
}
return options;
}

View file

@ -47,6 +47,22 @@ import java.util.*;
*/
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`.
*
*
* Default is PayLifeCostLevel.allAbilities.
*/
enum PayLifeCostLevel {
allAbilities,
nonSpellnonActivatedAbilities,
onlyManaAbilities,
none
}
/**
* Current player is real life player (human). Try to use in GUI and network engine only.
* <p>
@ -147,12 +163,12 @@ public interface Player extends MageItem, Copyable<Player> {
/**
* Is the player allowed to pay life for casting spells or activate activated abilities
*
* @param canPayLifeCost
* @param payLifeCostLevel
*/
void setCanPayLifeCost(boolean canPayLifeCost);
void setPayLifeCostLevel(PayLifeCostLevel payLifeCostLevel);
boolean getCanPayLifeCost();
PayLifeCostLevel getPayLifeCostLevel();
/**
* Can the player pay life to cast or activate the given ability

View file

@ -135,7 +135,7 @@ public abstract class PlayerImpl implements Player, Serializable {
protected boolean isTestMode = false;
protected boolean canGainLife = true;
protected boolean canLoseLife = true;
protected boolean canPayLifeCost = true;
protected PayLifeCostLevel payLifeCostLevel = PayLifeCostLevel.allAbilities;
protected boolean loseByZeroOrLessLife = true;
protected boolean canPlayCardsFromGraveyard = true;
protected boolean drawsOnOpponentsTurn = false;
@ -248,7 +248,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.userData = player.userData;
this.matchPlayer = player.matchPlayer;
this.canPayLifeCost = player.canPayLifeCost;
this.payLifeCostLevel = player.payLifeCostLevel;
this.sacrificeCostFilter = player.sacrificeCostFilter;
this.alternativeSourceCosts.addAll(player.alternativeSourceCosts);
this.storedBookmark = player.storedBookmark;
@ -344,7 +344,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.inRange.clear();
this.inRange.addAll(player.getInRange());
this.canPayLifeCost = player.getCanPayLifeCost();
this.payLifeCostLevel = player.getPayLifeCostLevel();
this.sacrificeCostFilter = player.getSacrificeCostFilter() != null
? player.getSacrificeCostFilter().copy() : null;
this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife();
@ -469,7 +469,7 @@ public abstract class PlayerImpl implements Player, Serializable {
this.maxAttackedBy = Integer.MAX_VALUE;
this.canGainLife = true;
this.canLoseLife = true;
this.canPayLifeCost = true;
this.payLifeCostLevel = PayLifeCostLevel.allAbilities;
this.sacrificeCostFilter = null;
this.loseByZeroOrLessLife = true;
this.canPlayCardsFromGraveyard = false;
@ -4348,22 +4348,32 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override
public boolean canPayLifeCost(Ability ability) {
if (!canPayLifeCost
&& (AbilityType.ACTIVATED.equals(ability.getAbilityType())
|| AbilityType.SPELL.equals(ability.getAbilityType()))) {
if (!isLifeTotalCanChange()) {
return false;
}
switch (payLifeCostLevel) {
case allAbilities:
return true;
case onlyManaAbilities:
return ability.getAbilityType() == AbilityType.MANA;
case nonSpellnonActivatedAbilities:
return ability.getAbilityType() != AbilityType.ACTIVATED && ability.getAbilityType() != AbilityType.SPELL;
case none:
default:
return false;
}
return isLifeTotalCanChange();
}
@Override
public boolean getCanPayLifeCost() {
return canPayLifeCost;
public PayLifeCostLevel getPayLifeCostLevel() {
return payLifeCostLevel;
}
@Override
public void setCanPayLifeCost(boolean canPayLifeCost) {
this.canPayLifeCost = canPayLifeCost;
public void setPayLifeCostLevel(PayLifeCostLevel payLifeCostLevel) {
this.payLifeCostLevel = payLifeCostLevel;
}
@Override