From b14af422804fdd92b1973d139c919e66348730e2 Mon Sep 17 00:00:00 2001 From: Alex Vasile <48962821+Alex-Vasile@users.noreply.github.com> Date: Sat, 17 Sep 2022 22:37:56 -0400 Subject: [PATCH] [DMU] Karn's Sylex (#9507) --- .../java/mage/player/ai/ComputerPlayer.java | 12 +- .../src/mage/cards/a/AngelOfJubilation.java | 2 +- Mage.Sets/src/mage/cards/k/KarnsSylex.java | 107 ++++++++++++++++++ Mage.Sets/src/mage/sets/DominariaUnited.java | 1 + .../continuous/AngelOfJubilationTest.java | 2 - .../test/cards/single/dmu/KarnsSylexTest.java | 79 +++++++++++++ .../java/org/mage/test/player/TestPlayer.java | 8 +- .../java/org/mage/test/stub/PlayerStub.java | 7 +- .../mage/abilities/ActivatedAbilityImpl.java | 6 +- .../mage/abilities/costs/mana/ManaCost.java | 15 +++ .../abilities/costs/mana/ManaCostImpl.java | 13 ++- .../abilities/costs/mana/ManaCostsImpl.java | 7 +- Mage/src/main/java/mage/players/Player.java | 22 +++- .../main/java/mage/players/PlayerImpl.java | 34 ++++-- 14 files changed, 285 insertions(+), 30 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/k/KarnsSylex.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index 8cece00e95..617bc18285 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -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) diff --git a/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java b/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java index 6dd7137114..ac201550d3 100644 --- a/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java +++ b/Mage.Sets/src/mage/cards/a/AngelOfJubilation.java @@ -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; diff --git a/Mage.Sets/src/mage/cards/k/KarnsSylex.java b/Mage.Sets/src/mage/cards/k/KarnsSylex.java new file mode 100644 index 0000000000..e6b19f74ad --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KarnsSylex.java @@ -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; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/DominariaUnited.java b/Mage.Sets/src/mage/sets/DominariaUnited.java index 2bbddcb348..317d405168 100644 --- a/Mage.Sets/src/mage/sets/DominariaUnited.java +++ b/Mage.Sets/src/mage/sets/DominariaUnited.java @@ -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)); 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 e23bb86fd4..e38ddf5840 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 @@ -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(); - } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java new file mode 100644 index 0000000000..5ad1afd3b6 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dmu/KarnsSylexTest.java @@ -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); + } +} 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 f36fc81980..ed12d94b48 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 @@ -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 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 307ce4acf6..fc24ddd487 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 @@ -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 diff --git a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java index 62cef1c997..81dadb57b6 100644 --- a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java @@ -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) { diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java index 91a3f0194f..b0d51dca44 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java @@ -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. + *
+ * 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();
diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java
index 59122a61ed..83823b9447 100644
--- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java
+++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java
@@ -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
diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java
index 7a01037e0e..d5a13f3e1e 100644
--- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java
+++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java
@@ -542,9 +542,14 @@ public class ManaCostsImpl
@@ -147,12 +163,12 @@ public interface Player extends MageItem, Copyable