mirror of
https://github.com/correl/mage.git
synced 2024-11-22 03:00:11 +00:00
[DMU] Karn's Sylex (#9507)
This commit is contained in:
parent
adbebfdd3e
commit
b14af42280
14 changed files with 285 additions and 30 deletions
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
107
Mage.Sets/src/mage/cards/k/KarnsSylex.java
Normal file
107
Mage.Sets/src/mage/cards/k/KarnsSylex.java
Normal 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[]{}, "");
|
||||
|
||||
// Karn’s Sylex
|
||||
this.addAbility(new EntersBattlefieldTappedAbility());
|
||||
|
||||
// Players can’t pay life to cast spells or to activate abilities that aren’t mana abilities.
|
||||
this.addAbility(new SimpleStaticAbility(new KarnsSylexEffect()));
|
||||
|
||||
// {X}, {T}, Exile Karn’s 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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -229,7 +229,6 @@ public class AngelOfJubilationTest extends CardTestPlayerBase {
|
|||
* 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);
|
||||
|
@ -244,6 +243,5 @@ public class AngelOfJubilationTest extends CardTestPlayerBase {
|
|||
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
* Karn’s Sylex enters the battlefield tapped.
|
||||
* Players can’t pay life to cast spells or to activate abilities that aren’t mana abilities.
|
||||
* {X}, {T}, Exile Karn’s 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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -69,7 +69,18 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost {
|
|||
|
||||
@Override
|
||||
public ManaOptions getOptions() {
|
||||
return options;
|
||||
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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
return isLifeTotalCanChange();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
|
|
Loading…
Reference in a new issue