From b5acf6477214d200917a3f2bdbd047e7a7613f8e Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 11 Feb 2020 22:29:07 +0400 Subject: [PATCH] * Monohybrid mana cost improves: * fixed wrong manually pay by mana pool (it pays generic cost instead colored part of monohybrid); * fixed not working cost reduction effects (now monohybrid cost will be reduced correctly with some limitation, see #6130); --- .../java/mage/player/ai/ComputerPlayer.java | 10 +- .../cost/modification/CostReduceTest.java | 159 ++++++++++++++++++ .../MonohybridCostReduceTest.java | 32 ++++ .../modification/MonohybridManaPayTest.java | 147 ++++++++++++++++ .../mage/abilities/costs/mana/ManaCosts.java | 10 +- .../abilities/costs/mana/ManaCostsImpl.java | 93 ++++++---- .../costs/mana/MonoHybridManaCost.java | 49 +++--- Mage/src/main/java/mage/players/ManaPool.java | 19 +++ Mage/src/main/java/mage/util/CardUtil.java | 148 +++++++++++++--- 9 files changed, 589 insertions(+), 78 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/MonohybridCostReduceTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/MonohybridManaPayTest.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 717aae4c8c7..58e18c17a4b 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 @@ -1389,10 +1389,12 @@ public class ComputerPlayer extends PlayerImpl implements Player { public boolean playMana(Ability ability, ManaCost unpaid, String promptText, Game game) { payManaMode = true; currentUnpaidMana = unpaid; - boolean result = playManaHandling(ability, unpaid, game); - currentUnpaidMana = null; - payManaMode = false; - return result; + try { + return playManaHandling(ability, unpaid, game); + } finally { + currentUnpaidMana = null; + payManaMode = false; + } } protected boolean playManaHandling(Ability ability, ManaCost unpaid, final Game game) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceTest.java new file mode 100644 index 00000000000..ee506ea8387 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/CostReduceTest.java @@ -0,0 +1,159 @@ +package org.mage.test.cards.cost.modification; + +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.mana.ManaCosts; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.util.CardUtil; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class CostReduceTest extends CardTestPlayerBase { + + private void testReduce(String sourceCost, int reduceAmount, String needReducedCost) { + // load mana by sctrict mode (e.g. for real mana usage in param) + ManaCosts source = new ManaCostsImpl<>(); + source.load(sourceCost, true); + ManaCosts need = new ManaCostsImpl<>(); + need.load(needReducedCost, true); + ManaCosts reduced = CardUtil.reduceCost(source, reduceAmount); + + if (!reduced.getText().equals(need.getText())) { + Assert.fail(sourceCost + " after reduction by " + reduceAmount + " must be " + need.getText() + ", but get " + reduced.getText()); + } + } + + @Test + public void test_Monohybrid() { + // extra test to ensure about mono hybrid test code + ManaCosts testCost = new ManaCostsImpl<>(); + testCost.load("{1/R}"); + Assert.assertEquals("normal mono hybrid always 2 generics", "{2/R}", testCost.getText()); + testCost = new ManaCostsImpl<>(); + testCost.load("{1/R}", true); + Assert.assertEquals("test mono hybrid have variant generic", "{1/R}", testCost.getText()); + testReduce("{5/R}", 0, "{5/R}"); // ensure that mono hybrid in test mode + + // DECREASE COST + + // colorless is not reduce + testReduce("{C}", 1, "{C}"); + testReduce("{C}{G}", 1, "{C}{G}"); + + // 0 generic, decrease cost by 1 + testReduce("", 1, ""); + testReduce("{R}", 1, "{R}"); + testReduce("{R}{G}", 1, "{R}{G}"); + + // 1 generic, decrease cost by 1 + testReduce("{1}", 1, ""); + testReduce("{R}{1}", 1, "{R}"); + testReduce("{1}{R}", 1, "{R}"); + testReduce("{1}{R}{G}", 1, "{R}{G}"); + testReduce("{R}{1}{G}", 1, "{R}{G}"); + testReduce("{R}{G}{1}", 1, "{R}{G}"); + + // 2 generics, decrease cost by 1 + testReduce("{2}", 1, "{1}"); + testReduce("{R}{2}", 1, "{R}{1}"); + testReduce("{2}{R}", 1, "{1}{R}"); + testReduce("{2}{R}{G}", 1, "{1}{R}{G}"); + testReduce("{R}{2}{G}", 1, "{R}{1}{G}"); + testReduce("{R}{G}{2}", 1, "{R}{G}{1}"); + + // 3 generics, decrease cost by 2 + testReduce("{2}", 2, ""); + testReduce("{3}", 2, "{1}"); + testReduce("{R}{3}", 2, "{R}{1}"); + testReduce("{3}{R}", 2, "{1}{R}"); + testReduce("{3}{R}{G}", 2, "{1}{R}{G}"); + testReduce("{R}{3}{G}", 2, "{R}{1}{G}"); + testReduce("{R}{G}{3}", 2, "{R}{G}{1}"); + + // INCREASE COST + + // colorless, increase cost by 1 + testReduce("{C}", -1, "{C}{1}"); + testReduce("{C}{G}", -1, "{C}{G}{1}"); + + // 0 generic, increase cost by 1 + testReduce("", -1, "{1}"); + testReduce("{R}", -1, "{R}{1}"); + testReduce("{R}{G}", -1, "{R}{G}{1}"); + + // 1 generic, increase cost by 1 + testReduce("{1}", -1, "{2}"); + testReduce("{R}{1}", -1, "{R}{2}"); + testReduce("{1}{R}", -1, "{2}{R}"); + testReduce("{1}{R}{G}", -1, "{2}{R}{G}"); + testReduce("{R}{1}{G}", -1, "{R}{2}{G}"); + testReduce("{R}{G}{1}", -1, "{R}{G}{2}"); + + // 2 generics, increase cost by 1 + testReduce("{2}", -1, "{3}"); + testReduce("{R}{2}", -1, "{R}{3}"); + testReduce("{2}{R}", -1, "{3}{R}"); + testReduce("{2}{R}{G}", -1, "{3}{R}{G}"); + testReduce("{R}{2}{G}", -1, "{R}{3}{G}"); + testReduce("{R}{G}{2}", -1, "{R}{G}{3}"); + + // 3 generics, increase cost by 2 + testReduce("{3}", -2, "{5}"); + testReduce("{R}{3}", -2, "{R}{5}"); + testReduce("{3}{R}", -2, "{5}{R}"); + testReduce("{3}{R}{G}", -2, "{5}{R}{G}"); + testReduce("{R}{3}{G}", -2, "{R}{5}{G}"); + testReduce("{R}{G}{3}", -2, "{R}{G}{5}"); + + // HYBRID + // from Reaper King + // If an effect reduces the cost to cast a spell by an amount of generic mana, it applies to a monocolored hybrid + // spell only if you’ve chosen a method of paying for it that includes generic mana. + // (2008-05-01) + + // MONO HYBRID + // 1. Mono hybrid always 2 generic mana like 2/R + // 2. Generic must have priority over hybrid + + // no generic, normal amount + // mono hybrid, decrease cost by 1 + testReduce("{2/R}", 1, "{1/R}"); + testReduce("{2/R}{2/G}", 1, "{1/R}{2/G}"); // TODO: add or/or reduction? (see https://github.com/magefree/mage/issues/6130 ) + // mono hybrid, increase cost by 1 + testReduce("{2/R}", -1, "{2/R}{1}"); + testReduce("{2/R}{2/G}", -1, "{2/R}{2/G}{1}"); + + // generic, normal amount + // mono hybrid + 1 generic, decrease cost by 1 + testReduce("{2/R}{1}", 1, "{2/R}"); + testReduce("{2/R}{2/G}{1}", 1, "{2/R}{2/G}"); + // mono hybrid + 1 generic, increase cost by 1 + testReduce("{2/R}{1}", -1, "{2/R}{2}"); + testReduce("{2/R}{2/G}{1}", -1, "{2/R}{2/G}{2}"); + + // generic, too much generic + // mono hybrid + 2 generic, decrease cost by 1 + testReduce("{2/R}{2}", 1, "{2/R}{1}"); + testReduce("{2/R}{2/G}{2}", 1, "{2/R}{2/G}{1}"); + // mono hybrid + 2 generic, increase cost by 1 + testReduce("{2/R}{2}", -1, "{2/R}{3}"); + testReduce("{2/R}{2/G}{2}", -1, "{2/R}{2/G}{3}"); + + // generic, too much reduce + // mono hybrid + 1 generic, decrease cost by 2 + testReduce("{2/R}{1}", 2, "{1/R}"); + testReduce("{2/R}{2/G}{1}", 2, "{1/R}{2/G}"); // TODO: add or/or reduction? (see https://github.com/magefree/mage/issues/6130 ) + // mono hybrid + 1 generic, increase cost by 2 + testReduce("{2/R}{1}", -2, "{2/R}{3}"); + testReduce("{2/R}{2/G}{1}", -2, "{2/R}{2/G}{3}"); + + // EXTRA + // TODO: add or/or reduction? (see https://github.com/magefree/mage/issues/6130 ) + testReduce("{2}{2/R}{2/G}", 3, "{1/R}{2/G}"); + testReduce("{2}{2/R}{2/G}", 4, "{0/R}{2/G}"); + testReduce("{2}{2/R}{2/G}", 5, "{0/R}{1/G}"); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/MonohybridCostReduceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/MonohybridCostReduceTest.java new file mode 100644 index 00000000000..cdd7b3a560c --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/MonohybridCostReduceTest.java @@ -0,0 +1,32 @@ +package org.mage.test.cards.cost.modification; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class MonohybridCostReduceTest extends CardTestPlayerBase { + + @Test + public void test_CostReduction_First() { + // monohybrid supports with some limitation -- it reduce first hybrid cost, see https://github.com/magefree/mage/issues/6130 + + // Artifact spells you cast cost {1} less to cast. + addCard(Zone.BATTLEFIELD, playerA, "Etherium Sculptor"); + // + // Reaper King + addCard(Zone.HAND, playerA, "Reaper King"); // {2/W}{2/U}{2/B}{2/R}{2/G} + // Add {C} + addCard(Zone.BATTLEFIELD, playerA, "Blinkmoth Nexus", 2 * 5 - 1); // one less to test cost reduction + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reaper King"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/MonohybridManaPayTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/MonohybridManaPayTest.java new file mode 100644 index 00000000000..379067302e5 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modification/MonohybridManaPayTest.java @@ -0,0 +1,147 @@ +package org.mage.test.cards.cost.modification; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class MonohybridManaPayTest extends CardTestPlayerBase { + + @Test + public void test_PaySimpleMana_Manually() { + // simulate user click on mana pool icons + disableManaAutoPayment(playerA); + + addCard(Zone.HAND, playerA, "Balduvian Bears"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + + // fill mana pool + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}"); + // cast spell + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears"); + // unlock mana order in pool + setChoice(playerA, "Green"); + setChoice(playerA, "Black"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Balduvian Bears", 1); + } + + @Test + public void test_PayMonohybridMana_ColorPart_SameUnlockOrder() { + // simulate user click on mana pool icons + // mono hybrid can be paid by color or {2} + disableManaAutoPayment(playerA); + + // Reaper King + addCard(Zone.HAND, playerA, "Reaper King"); // {2/W}{2/U}{2/B}{2/R}{2/G} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + + // fill mana pool + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}"); + // cast spell + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reaper King"); + // unlock mana order in pool (SAME ORDER AS PAYMENT - {W}{U}{B}{R}{G}) + setChoice(playerA, "White"); + setChoice(playerA, "Blue"); + setChoice(playerA, "Black"); + setChoice(playerA, "Red"); + setChoice(playerA, "Green"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Reaper King", 1); + } + + @Test + public void test_PayMonohybridMana_ColorPart_AnyUnlockOrder() { + // simulate user click on mana pool icons + // mono hybrid can be paid by color or {2} + disableManaAutoPayment(playerA); + + // Reaper King + addCard(Zone.HAND, playerA, "Reaper King"); // {2/W}{2/U}{2/B}{2/R}{2/G} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + + // fill mana pool + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}"); + // cast spell + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reaper King"); + // unlock mana order in pool (DIFFERENT ORDER AS PAYMENT - {R}{G}{W}{U}{B}) + setChoice(playerA, "Red"); + setChoice(playerA, "Green"); + setChoice(playerA, "White"); + setChoice(playerA, "Blue"); + setChoice(playerA, "Black"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Reaper King", 1); + } + + @Test + public void test_PayMonohybridMana_GenericPart_AnyUnlockOrder() { + // simulate user click on mana pool icons + // mono hybrid must be payed as color first + disableManaAutoPayment(playerA); // must pay by mana unlock command (like human clicks on mana pool icons) + + // Reaper King + addCard(Zone.HAND, playerA, "Reaper King"); // {2/W}{2/U}{2/B}{2/R}{2/G} + //addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); -- white mana paid by {2} + addCard(Zone.BATTLEFIELD, playerA, "Island", 1 + 2 + 2); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + //addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); -- red mana paid by {2} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + + // fill mana pool + //activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}"); -- paid by {2} + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 5); // pay for {U}, 2/W and 2/R + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}"); + //activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); -- paid by {2} + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}"); + // cast spell + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reaper King"); + // unlock mana order in pool (ANY ORDER) + setChoice(playerA, "Black"); + setChoice(playerA, "Green"); + setChoice(playerA, "Blue", 5); // unlocks and pays for U, 2/W and 2/R // TODO: add support to pay one by one, not all + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Reaper King", 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCosts.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCosts.java index 6aea5f0101f..f0da0138989 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCosts.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCosts.java @@ -29,7 +29,15 @@ public interface ManaCosts extends List, ManaCost { */ void setX(int xValue, int xPay); - void load(String mana); + default void load(String mana) { + load(mana, false); + } + + /** + * @param mana mana in strinct like "{2}{R}" or "{2/W}" + * @param extractMonoHybridGenericValue for tests only, extract generic mana value from mono hybrid string + */ + void load(String mana, boolean extractMonoHybridGenericValue); List getSymbols(); 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 a66d7a5702a..c92df834ddf 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java @@ -17,9 +17,11 @@ import mage.game.Game; import mage.players.ManaPool; import mage.players.Player; import mage.target.Targets; +import mage.util.CardUtil; import mage.util.ManaUtil; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; /** * @param @@ -30,7 +32,7 @@ public class ManaCostsImpl extends ArrayList implements M protected final UUID id; protected String text = null; - private static Map costs = new HashMap<>(); + private static Map costsCache = new ConcurrentHashMap<>(); // must be thread safe, can't use nulls public ManaCostsImpl() { this.id = UUID.randomUUID(); @@ -186,7 +188,7 @@ public class ManaCostsImpl extends ArrayList implements M tempCosts.pay(source, game, source.getSourceId(), player.getId(), false, null); } - + private void handleKrrikPhyrexianManaCosts(UUID payingPlayerId, Ability source, Game game) { Player player = game.getPlayer(payingPlayerId); if (this == null || player == null) { @@ -208,20 +210,16 @@ public class ManaCostsImpl extends ArrayList implements M /* find which color mana is in the cost and set it in the temp Phyrexian cost */ if (phyrexianColors.isWhite() && mana.getWhite() > 0) { tempPhyrexianCost = new PhyrexianManaCost(ColoredManaSymbol.W); - } - else if (phyrexianColors.isBlue() && mana.getBlue() > 0) { + } else if (phyrexianColors.isBlue() && mana.getBlue() > 0) { tempPhyrexianCost = new PhyrexianManaCost(ColoredManaSymbol.U); - } - else if (phyrexianColors.isBlack() && mana.getBlack() > 0) { + } else if (phyrexianColors.isBlack() && mana.getBlack() > 0) { tempPhyrexianCost = new PhyrexianManaCost(ColoredManaSymbol.B); - } - else if (phyrexianColors.isRed() && mana.getRed() > 0) { + } else if (phyrexianColors.isRed() && mana.getRed() > 0) { tempPhyrexianCost = new PhyrexianManaCost(ColoredManaSymbol.R); - } - else if (phyrexianColors.isGreen() && mana.getGreen() > 0) { + } else if (phyrexianColors.isGreen() && mana.getGreen() > 0) { tempPhyrexianCost = new PhyrexianManaCost(ColoredManaSymbol.G); } - + if (tempPhyrexianCost != null) { PayLifeCost payLifeCost = new PayLifeCost(2); if (payLifeCost.canPay(source, source.getSourceId(), player.getId(), game) @@ -297,18 +295,36 @@ public class ManaCostsImpl extends ArrayList implements M public void setPayment(Mana mana) { } + private boolean canPayColoredManaFromPool(ManaType needColor, ManaCost cost, ManaType canUseManaType, ManaPool pool) { + if (canUseManaType == null || canUseManaType.equals(needColor)) { + return cost.containsColor(CardUtil.manaTypeToColoredManaSymbol(needColor)) + && (pool.getColoredAmount(needColor) > 0 || pool.ConditionalManaHasManaType(needColor)); + } + return false; + } + @Override public void assignPayment(Game game, Ability ability, ManaPool pool, Cost costToPay) { - boolean wasUnlockedManaType = (pool.getUnlockedManaType() != null); - if (!pool.isAutoPayment() && !wasUnlockedManaType) { - // if auto payment is inactive and no mana type was clicked manually - do nothing - return; + // try to assign mana from pool to payment in priority order (color first) + + // auto-payment allows to use any mana type, if not then only unlocked can be used (mana type that were clicked in mana pool) + ManaType canUseManaType; + if (pool.isAutoPayment()) { + canUseManaType = null; // can use any type + } else { + canUseManaType = pool.getUnlockedManaType(); + if (canUseManaType == null) { + // auto payment is inactive and no mana type was clicked manually - do nothing + return; + } } + ManaCosts referenceCosts = null; if (pool.isForcedToPay()) { referenceCosts = this.copy(); } - // attempt to pay colorless costs (not generic) mana costs first + + // colorless costs (not generic) for (ManaCost cost : this) { if (!cost.isPaid() && cost instanceof ColorlessManaCost) { cost.assignPayment(game, ability, pool, costToPay); @@ -317,7 +333,8 @@ public class ManaCostsImpl extends ArrayList implements M } } } - //attempt to pay colored costs first + + // colored for (ManaCost cost : this) { if (!cost.isPaid() && cost instanceof ColoredManaCost) { cost.assignPayment(game, ability, pool, costToPay); @@ -327,6 +344,7 @@ public class ManaCostsImpl extends ArrayList implements M } } + // hybrid for (ManaCost cost : this) { if (!cost.isPaid() && cost instanceof HybridManaCost) { cost.assignPayment(game, ability, pool, costToPay); @@ -336,15 +354,15 @@ public class ManaCostsImpl extends ArrayList implements M } } - // Mono Hybrid mana costs - // First try only to pay colored mana or conditional colored mana with the pool + // monohybrid + // try to pay colored part for (ManaCost cost : this) { if (!cost.isPaid() && cost instanceof MonoHybridManaCost) { - if (((cost.containsColor(ColoredManaSymbol.W)) && (pool.getWhite() > 0 || pool.ConditionalManaHasManaType(ManaType.WHITE))) - || ((cost.containsColor(ColoredManaSymbol.B)) && (pool.getBlack() > 0 || pool.ConditionalManaHasManaType(ManaType.BLACK))) - || ((cost.containsColor(ColoredManaSymbol.R)) && (pool.getRed() > 0 || pool.ConditionalManaHasManaType(ManaType.RED))) - || ((cost.containsColor(ColoredManaSymbol.G)) && (pool.getGreen() > 0 || pool.ConditionalManaHasManaType(ManaType.GREEN))) - || ((cost.containsColor(ColoredManaSymbol.U)) && (pool.getBlue() > 0) || pool.ConditionalManaHasManaType(ManaType.BLUE))) { + if (canPayColoredManaFromPool(ManaType.WHITE, cost, canUseManaType, pool) + || canPayColoredManaFromPool(ManaType.BLACK, cost, canUseManaType, pool) + || canPayColoredManaFromPool(ManaType.RED, cost, canUseManaType, pool) + || canPayColoredManaFromPool(ManaType.GREEN, cost, canUseManaType, pool) + || canPayColoredManaFromPool(ManaType.BLUE, cost, canUseManaType, pool)) { cost.assignPayment(game, ability, pool, costToPay); if (pool.isEmpty() && pool.getConditionalMana().isEmpty()) { return; @@ -352,7 +370,7 @@ public class ManaCostsImpl extends ArrayList implements M } } } - // if colored didn't fit pay colorless with the mana + // try to pay generic part for (ManaCost cost : this) { if (!cost.isPaid() && cost instanceof MonoHybridManaCost) { cost.assignPayment(game, ability, pool, costToPay); @@ -362,6 +380,7 @@ public class ManaCostsImpl extends ArrayList implements M } } + // snow for (ManaCost cost : this) { if (!cost.isPaid() && cost instanceof SnowManaCost) { cost.assignPayment(game, ability, pool, costToPay); @@ -371,6 +390,7 @@ public class ManaCostsImpl extends ArrayList implements M } } + // generic for (ManaCost cost : this) { if (!cost.isPaid() && cost instanceof GenericManaCost) { cost.assignPayment(game, ability, pool, costToPay); @@ -380,14 +400,16 @@ public class ManaCostsImpl extends ArrayList implements M } } + // variable (generic) for (ManaCost cost : this) { if (!cost.isPaid() && cost instanceof VariableManaCost) { cost.assignPayment(game, ability, pool, costToPay); } } + // stop using mana of the clicked mana type pool.lockManaType(); - if (!wasUnlockedManaType) { + if (canUseManaType == null) { handleForcedToPayOnlyForCurrentPayment(game, pool, referenceCosts); } } @@ -420,10 +442,10 @@ public class ManaCostsImpl extends ArrayList implements M } @Override - public final void load(String mana) { + public final void load(String mana, boolean extractMonoHybridGenericValue) { this.clear(); - if (costs.containsKey(mana)) { - ManaCosts savedCosts = costs.get(mana); + if (!extractMonoHybridGenericValue && mana != null && costsCache.containsKey(mana)) { + ManaCosts savedCosts = costsCache.get(mana); for (ManaCost cost : savedCosts) { this.add(cost.copy()); } @@ -455,7 +477,14 @@ public class ManaCostsImpl extends ArrayList implements M this.add(new VariableManaCost(modifierForX)); } //TODO: handle multiple {X} and/or {Y} symbols } else if (Character.isDigit(symbol.charAt(0))) { - this.add(new MonoHybridManaCost(ColoredManaSymbol.lookup(symbol.charAt(2)))); + MonoHybridManaCost cost; + if (extractMonoHybridGenericValue) { + // for tests only, no usage in real game + cost = new MonoHybridManaCost(ColoredManaSymbol.lookup(symbol.charAt(2)), Integer.parseInt(symbol.substring(0, 1))); + } else { + cost = new MonoHybridManaCost(ColoredManaSymbol.lookup(symbol.charAt(2))); + } + this.add(cost); } else if (symbol.contains("P")) { this.add(new PhyrexianManaCost(ColoredManaSymbol.lookup(symbol.charAt(0)))); } else { @@ -463,7 +492,9 @@ public class ManaCostsImpl extends ArrayList implements M } } } - costs.put(mana, this.copy()); + if (!extractMonoHybridGenericValue) { + costsCache.put(mana, this.copy()); + } } } diff --git a/Mage/src/main/java/mage/abilities/costs/mana/MonoHybridManaCost.java b/Mage/src/main/java/mage/abilities/costs/mana/MonoHybridManaCost.java index e7c15468e3c..4ef23fa3963 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/MonoHybridManaCost.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/MonoHybridManaCost.java @@ -12,46 +12,53 @@ import java.util.List; public class MonoHybridManaCost extends ManaCostImpl { - private final ColoredManaSymbol mana; - private int mana2 = 2; + private final ColoredManaSymbol manaColor; + private int manaGeneric; - public MonoHybridManaCost(ColoredManaSymbol mana) { - this.mana = mana; - this.cost = new Mana(mana); - this.cost.add(Mana.GenericMana(2)); - addColoredOption(mana); - options.add(Mana.GenericMana(2)); + public MonoHybridManaCost(ColoredManaSymbol manaColor) { + this(manaColor, 2); + } + + public MonoHybridManaCost(ColoredManaSymbol manaColor, int genericAmount) { + this.manaColor = manaColor; + this.manaGeneric = genericAmount; + this.cost = new Mana(manaColor); + this.cost.add(Mana.GenericMana(genericAmount)); + addColoredOption(manaColor); + options.add(Mana.GenericMana(genericAmount)); } public MonoHybridManaCost(MonoHybridManaCost manaCost) { super(manaCost); - this.mana = manaCost.mana; - this.mana2 = manaCost.mana2; + this.manaColor = manaCost.manaColor; + this.manaGeneric = manaCost.manaGeneric; } @Override public int convertedManaCost() { - return 2; + // from wiki: A card with monocolored hybrid mana symbols in its mana cost has a converted mana cost equal to + // the highest possible cost it could be played for. Its converted mana cost never changes. + return Math.max(manaGeneric, 1); } @Override public boolean isPaid() { - if (paid || isColoredPaid(this.mana)) { + if (paid || isColoredPaid(this.manaColor)) { return true; } - return isColorlessPaid(this.mana2); + return isColorlessPaid(this.manaGeneric); } @Override public void assignPayment(Game game, Ability ability, ManaPool pool, Cost costToPay) { - if (!assignColored(ability, game, pool, mana, costToPay)) { - assignGeneric(ability, game, pool, mana2, null, costToPay); + if (!assignColored(ability, game, pool, manaColor, costToPay)) { + assignGeneric(ability, game, pool, manaGeneric, null, costToPay); } } @Override public String getText() { - return "{2/" + mana.toString() + '}'; + return "{" + manaGeneric + "/" + manaColor.toString() + '}'; } @Override @@ -61,7 +68,7 @@ public class MonoHybridManaCost extends ManaCostImpl { @Override public boolean testPay(Mana testMana) { - switch (mana) { + switch (manaColor) { case B: if (testMana.getBlack() > 0) { return true; @@ -93,18 +100,18 @@ public class MonoHybridManaCost extends ManaCostImpl { @Override public boolean containsColor(ColoredManaSymbol coloredManaSymbol) { - return mana == coloredManaSymbol; + return manaColor == coloredManaSymbol; } public ColoredManaSymbol getManaColor() { - return mana; + return manaColor; } @Override public List getManaOptions() { List manaList = new ArrayList<>(); - manaList.add(new Mana(mana)); - manaList.add(Mana.GenericMana(2)); + manaList.add(new Mana(manaColor)); + manaList.add(Mana.GenericMana(manaGeneric)); return manaList; } } diff --git a/Mage/src/main/java/mage/players/ManaPool.java b/Mage/src/main/java/mage/players/ManaPool.java index 2c5df2da273..dc9e6dfda97 100644 --- a/Mage/src/main/java/mage/players/ManaPool.java +++ b/Mage/src/main/java/mage/players/ManaPool.java @@ -456,4 +456,23 @@ public class ManaPool implements Serializable { manaItems.addAll(itemsCopy); } } + + public int getColoredAmount(ManaType manaType) { + switch (manaType) { + case BLACK: + return getBlack(); + case BLUE: + return getBlue(); + case GREEN: + return getGreen(); + case RED: + return getRed(); + case WHITE: + return getWhite(); + case GENERIC: + case COLORLESS: + default: + throw new IllegalArgumentException("Wrong mana type " + manaType); + } + } } diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 97df0a692db..0d437573f76 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -7,13 +7,16 @@ import mage.abilities.SpellAbility; import mage.abilities.costs.VariableCost; import mage.abilities.costs.mana.*; import mage.cards.Card; +import mage.constants.ColoredManaSymbol; import mage.constants.EmptyNames; +import mage.constants.ManaType; import mage.filter.Filter; import mage.game.CardState; import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.permanent.token.Token; import mage.util.functions.CopyTokenFunction; +import org.junit.Assert; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -90,34 +93,118 @@ public final class CardUtil { } private static ManaCosts adjustCost(ManaCosts manaCosts, int reduceCount) { - int restToReduce = reduceCount; ManaCosts adjustedCost = new ManaCostsImpl<>(); - boolean updated = false; - for (ManaCost manaCost : manaCosts) { - if (manaCost instanceof SnowManaCost) { - adjustedCost.add(manaCost); - continue; + + // nothing to change + if (reduceCount == 0) { + for (ManaCost manaCost : manaCosts) { + adjustedCost.add(manaCost.copy()); } - Mana mana = manaCost.getOptions().get(0); - int colorless = mana != null ? mana.getGeneric() : 0; - if (restToReduce != 0 && colorless > 0) { - if ((colorless - restToReduce) > 0) { - int newColorless = colorless - restToReduce; - adjustedCost.add(new GenericManaCost(newColorless)); - restToReduce = 0; - } else { - restToReduce -= colorless; + return adjustedCost; + } + + // remove or save cost + if (reduceCount > 0) { + int restToReduce = reduceCount; + + // first run - priority single option costs (generic) + for (ManaCost manaCost : manaCosts) { + if (manaCost instanceof SnowManaCost) { + adjustedCost.add(manaCost); + continue; } - updated = true; - } else { - adjustedCost.add(manaCost); + + if (manaCost.getOptions().size() == 0) { + adjustedCost.add(manaCost); + continue; + } + + // ignore monohybrid and other multi-option mana (for potential support) + if (manaCost.getOptions().size() > 1) { + continue; + } + + // generic mana reduce + Mana mana = manaCost.getOptions().get(0); + int colorless = mana != null ? mana.getGeneric() : 0; + if (restToReduce != 0 && colorless > 0) { + if ((colorless - restToReduce) > 0) { + // partly reduce + int newColorless = colorless - restToReduce; + adjustedCost.add(new GenericManaCost(newColorless)); + restToReduce = 0; + } else { + // full reduce - ignore cost + restToReduce -= colorless; + } + } else { + // nothing to reduce + adjustedCost.add(manaCost.copy()); + } + } + + // second run - priority for multi option costs (monohybrid) + // + // from Reaper King: + // If an effect reduces the cost to cast a spell by an amount of generic mana, it applies to a monocolored hybrid + // spell only if you’ve chosen a method of paying for it that includes generic mana. + // (2008-05-01) + // TODO: xmage don't use announce for hybrid mana (instead it uses auto-pay), so that's workaround uses first hybrid to reduce (see https://github.com/magefree/mage/issues/6130 ) + for (ManaCost manaCost : manaCosts) { + if (manaCost.getOptions().size() <= 1) { + continue; + } + + if (manaCost instanceof MonoHybridManaCost) { + // current implemention supports only 1 hybrid cost per object + MonoHybridManaCost mono = (MonoHybridManaCost) manaCost; + int colorless = mono.getOptions().get(1).getGeneric(); + if (restToReduce != 0 && colorless > 0) { + if ((colorless - restToReduce) > 0) { + // partly reduce + int newColorless = colorless - restToReduce; + adjustedCost.add(new MonoHybridManaCost(mono.getManaColor(), newColorless)); + restToReduce = 0; + } else { + // full reduce + adjustedCost.add(new MonoHybridManaCost(mono.getManaColor(), 0)); + restToReduce -= colorless; + } + } else { + // nothing to reduce + adjustedCost.add(mono.copy()); + } + continue; + } + + // unsupported multi-option mana types for reduce (like HybridManaCost) + adjustedCost.add(manaCost.copy()); } } - // for increasing spell cost effects - if (!updated && reduceCount < 0) { - adjustedCost.add(new GenericManaCost(-reduceCount)); + // increase cost (add to first generic or add new) + if (reduceCount < 0) { + Assert.assertEquals("must be empty", 0, adjustedCost.size()); + boolean added = false; + for (ManaCost manaCost : manaCosts) { + if (reduceCount != 0 && manaCost instanceof GenericManaCost) { + // add increase cost to existing generic + GenericManaCost gen = (GenericManaCost) manaCost; + adjustedCost.add(new GenericManaCost(gen.getOptions().get(0).getGeneric() + -reduceCount)); + reduceCount = 0; + added = true; + } else { + // non-generic mana + adjustedCost.add(manaCost.copy()); + } + } + if (!added) { + // add increase cost as new + adjustedCost.add(new GenericManaCost(-reduceCount)); + } } + + // cost modifying effects requiring snow mana unnecessarily (fixes #6000) Filter filter = manaCosts.stream() .filter(manaCost -> !(manaCost instanceof SnowManaCost)) .map(ManaCost::getSourceFilter) @@ -642,4 +729,23 @@ public final class CardUtil { res.addAll(mana2); return res; } + + public static ColoredManaSymbol manaTypeToColoredManaSymbol(ManaType manaType) { + switch (manaType) { + case BLACK: + return ColoredManaSymbol.B; + case BLUE: + return ColoredManaSymbol.U; + case GREEN: + return ColoredManaSymbol.G; + case RED: + return ColoredManaSymbol.R; + case WHITE: + return ColoredManaSymbol.W; + case GENERIC: + case COLORLESS: + default: + throw new IllegalArgumentException("Wrong mana type " + manaType); + } + } }