* 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);
This commit is contained in:
Oleg Agafonov 2020-02-11 22:29:07 +04:00
parent 13ad86cb21
commit b5acf64772
9 changed files with 589 additions and 78 deletions

View file

@ -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) {

View file

@ -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<ManaCost> source = new ManaCostsImpl<>();
source.load(sourceCost, true);
ManaCosts<ManaCost> need = new ManaCostsImpl<>();
need.load(needReducedCost, true);
ManaCosts<ManaCost> 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<ManaCost> 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 youve 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}");
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -29,7 +29,15 @@ public interface ManaCosts<T extends ManaCost> extends List<T>, 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<String> getSymbols();

View file

@ -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 <T>
@ -30,7 +32,7 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> implements M
protected final UUID id;
protected String text = null;
private static Map<String, ManaCosts> costs = new HashMap<>();
private static Map<String, ManaCosts> costsCache = new ConcurrentHashMap<>(); // must be thread safe, can't use nulls
public ManaCostsImpl() {
this.id = UUID.randomUUID();
@ -186,7 +188,7 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> implements M
}
@Override
public final void load(String mana) {
public final void load(String mana, boolean extractMonoHybridGenericValue) {
this.clear();
if (costs.containsKey(mana)) {
ManaCosts<ManaCost> savedCosts = costs.get(mana);
if (!extractMonoHybridGenericValue && mana != null && costsCache.containsKey(mana)) {
ManaCosts<ManaCost> savedCosts = costsCache.get(mana);
for (ManaCost cost : savedCosts) {
this.add(cost.copy());
}
@ -455,7 +477,14 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> implements M
}
}
}
costs.put(mana, this.copy());
if (!extractMonoHybridGenericValue) {
costsCache.put(mana, this.copy());
}
}
}

View file

@ -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<Mana> getManaOptions() {
List<Mana> 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;
}
}

View file

@ -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);
}
}
}

View file

@ -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<ManaCost> adjustCost(ManaCosts<ManaCost> manaCosts, int reduceCount) {
int restToReduce = reduceCount;
ManaCosts<ManaCost> 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 youve 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);
}
}
}