* 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

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