foul-magics/Mage/src/main/java/mage/util/CardUtil.java

1126 lines
43 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mage.util;
import com.google.common.collect.ImmutableList;
import mage.MageObject;
import mage.Mana;
import mage.abilities.Abilities;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.SpellAbility;
import mage.abilities.condition.Condition;
import mage.abilities.costs.VariableCost;
import mage.abilities.costs.mana.*;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.common.asthought.CanPlayCardControllerEffect;
import mage.abilities.effects.common.asthought.YouMaySpendManaAsAnyColorToCastTargetEffect;
import mage.abilities.hint.Hint;
import mage.abilities.hint.HintUtils;
import mage.cards.Card;
import mage.cards.MeldCard;
import mage.cards.ModalDoubleFacesCard;
import mage.cards.SplitCard;
import mage.constants.*;
import mage.filter.Filter;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.CardState;
import mage.game.Game;
import mage.game.command.Commander;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.PermanentMeld;
import mage.game.permanent.token.Token;
import mage.game.stack.Spell;
import mage.players.Player;
import mage.target.Target;
import mage.target.targetpointer.FixedTarget;
import mage.util.functions.CopyTokenFunction;
import org.apache.log4j.Logger;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author nantuko
*/
public final class CardUtil {
private static final Logger logger = Logger.getLogger(CardUtil.class);
public static final List<String> RULES_ERROR_INFO = ImmutableList.of("Exception occurred in rules generation");
private static final String SOURCE_EXILE_ZONE_TEXT = "SourceExileZone";
static final String[] numberStrings = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
"ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty"};
static final String[] ordinalStrings = {"first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eightth", "ninth",
"tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth"};
public static final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
/**
* Increase spell or ability cost to be paid.
*
* @param ability
* @param increaseCount
*/
public static void increaseCost(Ability ability, int increaseCount) {
adjustAbilityCost(ability, -increaseCount);
}
/**
* calculates the maximal possible generic mana reduction for a given mana cost
*
* @param mana mana costs that should be reduced
* @param maxPossibleReduction max possible generic mana reduction
* @param notLessThan the complete costs may not be reduced more than this CMC mana costs
*/
public static int calculateActualPossibleGenericManaReduction(Mana mana, int maxPossibleReduction, int notLessThan) {
int nonGeneric = mana.count() - mana.getGeneric();
int notPossibleGenericReduction = Math.max(0, notLessThan - nonGeneric);
int actualPossibleGenericManaReduction = Math.max(0, mana.getGeneric() - notPossibleGenericReduction);
if (actualPossibleGenericManaReduction > maxPossibleReduction) {
actualPossibleGenericManaReduction = maxPossibleReduction;
}
return actualPossibleGenericManaReduction;
}
/**
* Reduces ability cost to be paid.
*
* @param ability
* @param reduceCount
*/
public static void reduceCost(Ability ability, int reduceCount) {
adjustAbilityCost(ability, reduceCount);
}
/**
* Adjusts spell or ability cost to be paid.
*
* @param spellAbility
* @param reduceCount
*/
public static void adjustCost(SpellAbility spellAbility, int reduceCount) {
CardUtil.adjustAbilityCost(spellAbility, reduceCount);
}
public static ManaCosts<ManaCost> increaseCost(ManaCosts<ManaCost> manaCosts, int increaseCount) {
return adjustCost(manaCosts, -increaseCount);
}
public static ManaCosts<ManaCost> reduceCost(ManaCosts<ManaCost> manaCosts, int reduceCount) {
return adjustCost(manaCosts, reduceCount);
}
/**
* Adjusts ability cost to be paid.
*
* @param ability
* @param reduceCount
*/
private static void adjustAbilityCost(Ability ability, int reduceCount) {
ManaCosts<ManaCost> adjustedCost = adjustCost(ability.getManaCostsToPay(), reduceCount);
ability.getManaCostsToPay().clear();
ability.getManaCostsToPay().addAll(adjustedCost);
}
private static ManaCosts<ManaCost> adjustCost(ManaCosts<ManaCost> manaCosts, int reduceCount) {
ManaCosts<ManaCost> adjustedCost = new ManaCostsImpl<>();
// nothing to change
if (reduceCount == 0) {
for (ManaCost manaCost : manaCosts) {
adjustedCost.add(manaCost.copy());
}
return adjustedCost;
}
// remove or save cost
if (reduceCount > 0) {
int restToReduce = reduceCount;
// first run - priority for single option costs (generic)
for (ManaCost manaCost : manaCosts) {
if (manaCost instanceof SnowManaCost) {
adjustedCost.add(manaCost);
continue;
}
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 reduce from left to right hybrid cost without cost parts announce
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());
}
}
// increase cost (add to first generic or add new)
if (reduceCount < 0) {
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)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
if (filter != null) {
adjustedCost.setSourceFilter(filter);
}
return adjustedCost;
}
public static void reduceCost(SpellAbility spellAbility, ManaCosts<ManaCost> manaCostsToReduce) {
adjustCost(spellAbility, manaCostsToReduce, true);
}
public static void increaseCost(SpellAbility spellAbility, ManaCosts<ManaCost> manaCostsToIncrease) {
ManaCosts<ManaCost> increasedCost = spellAbility.getManaCostsToPay().copy();
for (ManaCost manaCost : manaCostsToIncrease) {
increasedCost.add(manaCost.copy());
}
spellAbility.getManaCostsToPay().clear();
spellAbility.getManaCostsToPay().addAll(increasedCost);
}
/**
* Adjusts spell or ability cost to be paid by colored and generic mana.
*
* @param spellAbility
* @param manaCostsToReduce costs to reduce
* @param convertToGeneric colored mana does reduce generic mana if no
* appropriate colored mana is in the costs
* included
*/
public static void adjustCost(SpellAbility spellAbility, ManaCosts<ManaCost> manaCostsToReduce, boolean convertToGeneric) {
ManaCosts<ManaCost> previousCost = spellAbility.getManaCostsToPay();
ManaCosts<ManaCost> adjustedCost = new ManaCostsImpl<>();
// save X value (e.g. convoke ability)
for (VariableCost vCost : previousCost.getVariableCosts()) {
if (vCost instanceof VariableManaCost) {
adjustedCost.add((VariableManaCost) vCost);
}
}
Mana reduceMana = new Mana();
for (ManaCost manaCost : manaCostsToReduce) {
if (manaCost instanceof MonoHybridManaCost) {
reduceMana.add(Mana.GenericMana(2));
} else {
reduceMana.add(manaCost.getMana());
}
}
ManaCosts<ManaCost> manaCostToCheckForGeneric = new ManaCostsImpl<>();
// subtract non-generic mana
for (ManaCost newManaCost : previousCost) {
Mana mana = newManaCost.getMana();
if (!(newManaCost instanceof MonoHybridManaCost) && mana.getGeneric() > 0) {
manaCostToCheckForGeneric.add(newManaCost);
continue;
}
boolean hybridMana = newManaCost instanceof HybridManaCost;
if (mana.getBlack() > 0 && reduceMana.getBlack() > 0) {
if (reduceMana.getBlack() > mana.getBlack()) {
reduceMana.setBlack(reduceMana.getBlack() - mana.getBlack());
mana.setBlack(0);
} else {
mana.setBlack(mana.getBlack() - reduceMana.getBlack());
reduceMana.setBlack(0);
}
if (hybridMana) {
continue;
}
}
if (mana.getRed() > 0 && reduceMana.getRed() > 0) {
if (reduceMana.getRed() > mana.getRed()) {
reduceMana.setRed(reduceMana.getRed() - mana.getRed());
mana.setRed(0);
} else {
mana.setRed(mana.getRed() - reduceMana.getRed());
reduceMana.setRed(0);
}
if (hybridMana) {
continue;
}
}
if (mana.getBlue() > 0 && reduceMana.getBlue() > 0) {
if (reduceMana.getBlue() > mana.getBlue()) {
reduceMana.setBlue(reduceMana.getBlue() - mana.getBlue());
mana.setBlue(0);
} else {
mana.setBlue(mana.getBlue() - reduceMana.getBlue());
reduceMana.setBlue(0);
}
if (hybridMana) {
continue;
}
}
if (mana.getGreen() > 0 && reduceMana.getGreen() > 0) {
if (reduceMana.getGreen() > mana.getGreen()) {
reduceMana.setGreen(reduceMana.getGreen() - mana.getGreen());
mana.setGreen(0);
} else {
mana.setGreen(mana.getGreen() - reduceMana.getGreen());
reduceMana.setGreen(0);
}
if (hybridMana) {
continue;
}
}
if (mana.getWhite() > 0 && reduceMana.getWhite() > 0) {
if (reduceMana.getWhite() > mana.getWhite()) {
reduceMana.setWhite(reduceMana.getWhite() - mana.getWhite());
mana.setWhite(0);
} else {
mana.setWhite(mana.getWhite() - reduceMana.getWhite());
reduceMana.setWhite(0);
}
if (hybridMana) {
continue;
}
}
if (mana.getColorless() > 0 && reduceMana.getColorless() > 0) {
if (reduceMana.getColorless() > mana.getColorless()) {
reduceMana.setColorless(reduceMana.getColorless() - mana.getColorless());
mana.setColorless(0);
} else {
mana.setColorless(mana.getColorless() - reduceMana.getColorless());
reduceMana.setColorless(0);
}
}
if (mana.count() > 0) {
if (newManaCost instanceof MonoHybridManaCost) {
if (mana.count() == 2) {
reduceMana.setGeneric(reduceMana.getGeneric() - 2);
continue;
}
}
manaCostToCheckForGeneric.add(newManaCost);
}
}
// subtract colorless mana, use all mana that is left
int reduceAmount;
if (convertToGeneric) {
reduceAmount = reduceMana.count();
} else {
reduceAmount = reduceMana.getGeneric();
}
if (reduceAmount > 0) {
for (ManaCost newManaCost : manaCostToCheckForGeneric) {
Mana mana = newManaCost.getMana();
if (mana.getGeneric() == 0 || reduceAmount == 0) {
adjustedCost.add(newManaCost);
continue;
}
if (newManaCost instanceof MonoHybridManaCost) {
if (reduceAmount > 1) {
reduceAmount -= 2;
mana.clear();
}
continue;
}
if (mana.getGeneric() > 0) {
if (reduceAmount > mana.getGeneric()) {
reduceAmount -= mana.getGeneric();
mana.setGeneric(0);
} else {
mana.setGeneric(mana.getGeneric() - reduceAmount);
reduceAmount = 0;
}
}
if (mana.count() > 0) {
adjustedCost.add(0, new GenericManaCost(mana.count()));
}
}
} else {
adjustedCost.addAll(manaCostToCheckForGeneric);
}
if (adjustedCost.isEmpty()) {
adjustedCost.add(new GenericManaCost(0)); // neede to check if cost was reduced to 0
}
adjustedCost.setSourceFilter(previousCost.getSourceFilter()); // keep mana source restrictions
spellAbility.getManaCostsToPay().clear();
spellAbility.getManaCostsToPay().addAll(adjustedCost);
}
/**
* Returns function that copies params\abilities from one card to
* {@link Token}.
*
* @param target
* @return
*/
public static CopyTokenFunction copyTo(Token target) {
return new CopyTokenFunction(target);
}
/**
* Converts an integer number to string Numbers > 20 will be returned as
* digits
*
* @param number
* @return
*/
public static String numberToText(int number) {
return numberToText(number, "one");
}
/**
* Converts an integer number to string like "one", "two", "three", ...
* Numbers > 20 will be returned as digits
*
* @param number number to convert to text
* @param forOne if the number is 1, this string will be returnedinstead of
* "one".
* @return
*/
public static String numberToText(int number, String forOne) {
if (number == 1 && forOne != null) {
return forOne;
}
if (number >= 0 && number < 21) {
return numberStrings[number];
}
if (number == Integer.MAX_VALUE) {
return "any number of";
}
return Integer.toString(number);
}
public static String numberToText(String number) {
return numberToText(number, "one");
}
public static String numberToText(String number, String forOne) {
if (checkNumeric(number)) {
return numberToText(Integer.parseInt(number), forOne);
}
return number;
}
public static String numberToOrdinalText(int number) {
if (number >= 1 && number < 21) {
return ordinalStrings[number - 1];
}
return number + "th";
}
public static String replaceSourceName(String message, String sourceName) {
return message.replace("{this}", sourceName);
}
public static String booleanToFlipName(boolean flip) {
if (flip) {
return "Heads";
}
return "Tails";
}
public static boolean checkNumeric(String s) {
return s.chars().allMatch(Character::isDigit);
}
/**
* Parse card number as int (support base [123] and alternative numbers
* [123b], [U123]).
*
* @param cardNumber origin card number
* @return int
*/
public static int parseCardNumberAsInt(String cardNumber) {
if (cardNumber.isEmpty()) {
throw new IllegalArgumentException("Card number is empty.");
}
try {
if (!Character.isDigit(cardNumber.charAt(0))) {
// U123
return Integer.parseInt(cardNumber.substring(1));
} else if (!Character.isDigit(cardNumber.charAt(cardNumber.length() - 1))) {
// 123b
return Integer.parseInt(cardNumber.substring(0, cardNumber.length() - 1));
} else {
// 123
return Integer.parseInt(cardNumber);
}
} catch (NumberFormatException e) {
// wrong numbers like RA5 and etc
return -1;
}
}
/**
* Creates and saves a (card + zoneChangeCounter) specific exileId.
*
* @param game the current game
* @param source source ability
* @return the specific UUID
*/
public static UUID getCardExileZoneId(Game game, Ability source) {
return getCardExileZoneId(game, source.getSourceId());
}
public static UUID getCardExileZoneId(Game game, UUID sourceId) {
return getCardExileZoneId(game, sourceId, false);
}
public static UUID getCardExileZoneId(Game game, UUID sourceId, boolean previous) {
return getExileZoneId(getCardZoneString(SOURCE_EXILE_ZONE_TEXT, sourceId, game, previous), game);
}
public static UUID getExileZoneId(Game game, UUID objectId, int zoneChangeCounter) {
return getExileZoneId(getObjectZoneString(SOURCE_EXILE_ZONE_TEXT, objectId, game, zoneChangeCounter, false), game);
}
public static UUID getExileZoneId(String key, Game game) {
UUID exileId = (UUID) game.getState().getValue(key);
if (exileId == null) {
exileId = UUID.randomUUID();
game.getState().setValue(key, exileId);
}
return exileId;
}
/**
* Creates a string from text + cardId and the zoneChangeCounter of the card
* (from cardId). This string can be used to save and get values that must
* be specific to a permanent instance. So they won't match, if a permanent
* was e.g. exiled and came back immediately.
*
* @param text short value to describe the value
* @param cardId id of the card
* @param game the game
* @return
*/
public static String getCardZoneString(String text, UUID cardId, Game game) {
return getCardZoneString(text, cardId, game, false);
}
public static String getCardZoneString(String text, UUID cardId, Game game, boolean previous) {
int zoneChangeCounter = 0;
Card card = game.getCard(cardId); // if called for a token, the id is enough
if (card != null) {
zoneChangeCounter = card.getZoneChangeCounter(game);
}
return getObjectZoneString(text, cardId, game, zoneChangeCounter, previous);
}
public static String getObjectZoneString(String text, MageObject mageObject, Game game) {
int zoneChangeCounter = 0;
if (mageObject instanceof Permanent) {
zoneChangeCounter = mageObject.getZoneChangeCounter(game);
} else if (mageObject instanceof Card) {
zoneChangeCounter = mageObject.getZoneChangeCounter(game);
}
return getObjectZoneString(text, mageObject.getId(), game, zoneChangeCounter, false);
}
public static String getObjectZoneString(String text, UUID objectId, Game game, int zoneChangeCounter, boolean previous) {
StringBuilder uniqueString = new StringBuilder();
if (text != null) {
uniqueString.append(text);
}
uniqueString.append(objectId);
uniqueString.append(previous ? zoneChangeCounter - 1 : zoneChangeCounter);
return uniqueString.toString();
}
/**
* Adds tags to mark the additional info of a card (e.g. blue font color)
*
* @param text text body
* @return
*/
public static String addToolTipMarkTags(String text) {
return "<font color = 'blue'>" + text + "</font>";
}
public static boolean cardCanBePlayedNow(Card card, UUID playerId, Game game) {
if (card.isLand()) {
return game.canPlaySorcery(playerId) && game.getPlayer(playerId).canPlayLand();
} else {
return card.getSpellAbility() != null && card.getSpellAbility().spellCanBeActivatedRegularlyNow(playerId, game);
}
}
public static int addWithOverflowCheck(int base, int increment) {
long result = ((long) base) + increment;
if (result > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
} else if (result < Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
return base + increment;
}
public static int subtractWithOverflowCheck(int base, int decrement) {
long result = ((long) base) - decrement;
if (result > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
} else if (result < Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
return base - decrement;
}
public static int multiplyWithOverflowCheck(int base, int multiply) {
long result = ((long) base) * multiply;
if (result > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
} else if (result < Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
return base * multiply;
}
public static String createObjectRealtedWindowTitle(Ability source, Game game, String textSuffix) {
String title;
if (source != null) {
MageObject sourceObject = game.getObject(source.getSourceId());
if (sourceObject != null) {
title = sourceObject.getIdName()
+ " [" + source.getSourceObjectZoneChangeCounter() + "]"
+ (textSuffix == null ? "" : " " + textSuffix);
} else {
title = textSuffix == null ? "" : textSuffix;
}
} else {
title = textSuffix == null ? "" : textSuffix;
}
return title;
}
/**
* Face down cards and their copy tokens don't have names and that's "empty"
* names is not equals
*/
public static boolean haveSameNames(String name1, String name2, Boolean ignoreMtgRuleForEmptyNames) {
if (ignoreMtgRuleForEmptyNames) {
// simple compare for tests and engine
return name1 != null && name1.equals(name2);
} else {
// mtg logic compare for game (empty names can't be same)
return !haveEmptyName(name1) && !haveEmptyName(name2) && name1.equals(name2);
}
}
public static boolean haveSameNames(String name1, String name2) {
return haveSameNames(name1, name2, false);
}
public static boolean haveSameNames(MageObject object1, MageObject object2) {
return object1 != null && object2 != null && haveSameNames(object1.getName(), object2.getName());
}
public static boolean haveSameNames(MageObject object, String needName, Game game) {
return containsName(object, needName, game);
}
public static boolean containsName(MageObject object, String name, Game game) {
return new NamePredicate(name).apply(object, game);
}
public static boolean haveEmptyName(String name) {
return name == null || name.isEmpty() || name.equals(EmptyNames.FACE_DOWN_CREATURE.toString()) || name.equals(EmptyNames.FACE_DOWN_TOKEN.toString());
}
public static boolean haveEmptyName(MageObject object) {
return object == null || haveEmptyName(object.getName());
}
public static UUID getMainCardId(Game game, UUID objectId) {
Card card = game.getCard(objectId);
return card != null ? card.getMainCard().getId() : objectId;
}
public static String urlEncode(String data) {
if (data.isEmpty()) {
return "";
}
try {
return URLEncoder.encode(data, "UTF-8");
} catch (UnsupportedEncodingException e) {
return "";
}
}
public static String urlDecode(String encodedData) {
if (encodedData.isEmpty()) {
return "";
}
try {
return URLDecoder.decode(encodedData, "UTF-8");
} catch (UnsupportedEncodingException e) {
return "";
}
}
/**
* Checks if a card had a given ability depending their historic cardState
*
* @param ability the ability that is checked
* @param cardState the historic cardState (from LKI)
* @param cardId the id of the card
* @param game
* @return
*/
public static boolean cardHadAbility(Ability ability, CardState cardState, UUID cardId, Game game) {
Card card = game.getCard(cardId);
if (card != null) {
if (cardState != null) {
if (cardState.getAbilities().contains(ability)) { // Check other abilities (possibly given after lost of abilities)
return true;
}
if (cardState.hasLostAllAbilities()) {
return false; // Not allowed to check abilities of original card
}
}
return card.getAbilities().contains(ability); // check if the original card has the ability
}
return false;
}
public static List<String> concatManaSymbols(String delimeter, List<String> mana1, List<String> mana2) {
List<String> res = new ArrayList<>(mana1);
if (res.size() > 0 && mana2.size() > 0 && delimeter != null && !delimeter.isEmpty()) {
res.add(delimeter);
}
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);
}
}
public static String getBoostCountAsStr(int power, int toughness) {
// sign fix for zero values
// -1/+0 must be -1/-0
// +0/-1 must be -0/-1
String signedP = String.format("%1$+d", power);
String signedT = String.format("%1$+d", toughness);
if (signedP.equals("+0") && signedT.startsWith("-")) {
signedP = "-0";
}
if (signedT.equals("+0") && signedP.startsWith("-")) {
signedT = "-0";
}
return signedP + "/" + signedT;
}
public static boolean isSpliceAbility(Ability ability, Game game) {
if (ability instanceof SpellAbility) {
return ((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.SPLICE;
}
return false;
}
public static boolean isFusedPartAbility(Ability ability, Game game) {
// TODO: does it work fine with copies of spells on stack?
if (ability instanceof SpellAbility) {
Spell mainSpell = game.getSpell(ability.getId());
if (mainSpell == null) {
return true;
} else {
SpellAbility mainSpellAbility = mainSpell.getSpellAbility();
return mainSpellAbility.getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED
&& !ability.equals(mainSpellAbility);
}
}
return false;
}
public static Abilities<Ability> getAbilities(MageObject object, Game game) {
if (object instanceof Card) {
return ((Card) object).getAbilities(game);
} else if (object instanceof Commander && Zone.COMMAND.equals(game.getState().getZone(object.getId()))) {
// Commanders in command zone must gain cost related abilities for playable
// calculation (affinity, convoke; example: Chief Engineer). So you must use card object here
Card card = game.getCard(object.getId());
if (card != null) {
return card.getAbilities(game);
} else {
return object.getAbilities();
}
} else {
return object.getAbilities();
}
}
public static String getTextWithFirstCharUpperCase(String text) {
if (text != null && text.length() >= 1) {
return Character.toUpperCase(text.charAt(0)) + text.substring(1);
} else {
return text;
}
}
public static Set<UUID> getAllSelectedTargets(Ability ability, Game game) {
return ability.getModes().getSelectedModes()
.stream()
.map(ability.getModes()::get)
.map(Mode::getTargets)
.flatMap(Collection::stream)
.map(Target::getTargets)
.flatMap(Collection::stream)
.collect(Collectors.toSet());
}
public static Set<UUID> getAllPossibleTargets(Ability ability, Game game) {
return ability.getModes().values()
.stream()
.map(Mode::getTargets)
.flatMap(Collection::stream)
.map(t -> t.possibleTargets(ability.getSourceId(), ability.getControllerId(), game))
.flatMap(Collection::stream)
.collect(Collectors.toSet());
}
/**
* Put card to battlefield without resolve (for cheats and tests only)
*
* @param source must be non null (if you need it empty then use fakeSourceAbility)
* @param game
* @param card
* @param player
*/
public static void putCardOntoBattlefieldWithEffects(Ability source, Game game, Card card, Player player) {
// same logic as ZonesHandler->maybeRemoveFromSourceZone
// prepare card and permanent
card.setZone(Zone.BATTLEFIELD, game);
card.setOwnerId(player.getId());
PermanentCard permanent;
if (card instanceof MeldCard) {
permanent = new PermanentMeld(card, player.getId(), game);
} else {
permanent = new PermanentCard(card, player.getId(), game);
}
// put onto battlefield with possible counters
game.getPermanentsEntering().put(permanent.getId(), permanent);
card.checkForCountersToAdd(permanent, source, game);
permanent.entersBattlefield(source, game, Zone.OUTSIDE, false);
game.addPermanent(permanent, game.getState().getNextPermanentOrderNumber());
game.getPermanentsEntering().remove(permanent.getId());
// workaround for special tapped status from test framework's command (addCard)
if (card instanceof PermanentCard && ((PermanentCard) card).isTapped()) {
permanent.setTapped(true);
}
// remove sickness
permanent.removeSummoningSickness();
// init effects on static abilities (init continuous effects; warning, game state contains copy)
for (ContinuousEffect effect : game.getState().getContinuousEffects().getLayeredEffects(game)) {
Optional<Ability> ability = game.getState().getContinuousEffects().getLayeredEffectAbilities(effect).stream().findFirst();
if (ability.isPresent() && permanent.getId().equals(ability.get().getSourceId())) {
effect.init(ability.get(), game, player.getId());
}
}
}
/**
* Return card name for same name searching
*
* @param card
* @return
*/
public static String getCardNameForSameNameSearch(Card card) {
// it's ok to return one name only cause NamePredicate can find same card by first name
if (card instanceof SplitCard) {
return ((SplitCard) card).getLeftHalfCard().getName();
} else if (card instanceof ModalDoubleFacesCard) {
return ((ModalDoubleFacesCard) card).getLeftHalfCard().getName();
} else {
return card.getName();
}
}
public static List<String> getCardRulesWithAdditionalInfo(UUID cardId, String cardName,
Abilities<Ability> rulesSource, Abilities<Ability> hintAbilities) {
return getCardRulesWithAdditionalInfo(null, cardId, cardName, rulesSource, hintAbilities);
}
/**
* Prepare rules list from abilities
*
* @param rulesSource abilities list to show as rules
* @param hintsSource abilities list to show as card hints only (you can add additional hints here; exameple: from second or transformed side)
*/
public static List<String> getCardRulesWithAdditionalInfo(Game game, UUID cardId, String cardName,
Abilities<Ability> rulesSource, Abilities<Ability> hintsSource) {
try {
List<String> rules = rulesSource.getRules(cardName);
if (game != null) {
// debug state
for (String data : game.getState().getCardState(cardId).getInfo().values()) {
rules.add(data);
}
// ability hints
List<String> abilityHints = new ArrayList<>();
if (HintUtils.ABILITY_HINTS_ENABLE) {
for (Ability ability : hintsSource) {
for (Hint hint : ability.getHints()) {
String s = hint.getText(game, ability);
if (s != null && !s.isEmpty()) {
abilityHints.add(s);
}
}
}
}
// restrict hints only for permanents, not cards
// total hints
if (!abilityHints.isEmpty()) {
rules.add(HintUtils.HINT_START_MARK);
HintUtils.appendHints(rules, abilityHints);
}
}
return rules;
} catch (Exception e) {
logger.error("Exception in rules generation for card: " + cardName, e);
}
return RULES_ERROR_INFO;
}
/**
* Take control under another player, use it in inner effects like Word of Commands. Don't forget to end it in same code.
*
* @param game
* @param controller
* @param targetPlayer
* @param givePauseForResponse if you want to give controller time to watch opponent's hand (if you remove control effect in the end of code)
*/
public static void takeControlUnderPlayerStart(Game game, Player controller, Player targetPlayer, boolean givePauseForResponse) {
controller.controlPlayersTurn(game, targetPlayer.getId());
if (givePauseForResponse) {
while (controller.canRespond()) {
if (controller.chooseUse(Outcome.Benefit, "You got control of " + targetPlayer.getLogName()
+ ". Use switch hands button to view opponent's hand.", null,
"Continue", "Wait", null, game)) {
break;
}
}
}
}
/**
* Return control under another player, use it in inner effects like Word of Commands.
*
* @param game
* @param controller
* @param targetPlayer
*/
public static void takeControlUnderPlayerEnd(Game game, Player controller, Player targetPlayer) {
targetPlayer.setGameUnderYourControl(true, false);
if (!targetPlayer.getTurnControlledBy().equals(controller.getId())) {
controller.getPlayersUnderYourControl().remove(targetPlayer.getId());
}
}
public static void makeCardPlayableAndSpendManaAsAnyColor(Game game, Ability source, Card card, Duration duration) {
makeCardPlayableAndSpendManaAsAnyColor(game, source, card, duration, null);
}
/**
* Add effects to game that allows to play/cast card from current zone and spend mana as any type for it.
* Effects will be discarded/ignored on any card movements or blinks (after ZCC change)
* <p>
* Affected to all card's parts
*
* @param game
* @param card
* @param duration
* @param condition can be null
*/
public static void makeCardPlayableAndSpendManaAsAnyColor(Game game, Ability source, Card card, Duration duration, Condition condition) {
// Effect can be used for cards in zones and permanents on battlefield
// PermanentCard's ZCC is static, but we need updated ZCC from the card (after moved to another zone)
// So there is a workaround to get actual card's ZCC
// Example: Hostage Taker
UUID objectId = card.getMainCard().getId();
int zcc = game.getState().getZoneChangeCounter(objectId);
game.addEffect(new CanPlayCardControllerEffect(game, objectId, zcc, duration, condition), source);
game.addEffect(new YouMaySpendManaAsAnyColorToCastTargetEffect(duration, condition).setTargetPointer(new FixedTarget(objectId, zcc)), source);
}
/**
* Pay life in effects
*
* @param lifeToPay
* @param player
* @param source
* @param game
* @return true on good pay
*/
public static boolean tryPayLife(int lifeToPay, Player player, Ability source, Game game) {
// rules:
// 119.4. If a cost or effect allows a player to pay an amount of life greater than 0, the player may do so
// only if their life total is greater than or equal to the amount of the payment. If a player pays life,
// the payment is subtracted from their life total; in other words, the player loses that much life.
// (Players can always pay 0 life.)
if (lifeToPay == 0) {
return true;
} else if (lifeToPay < 0) {
return false;
}
if (lifeToPay > player.getLife()) {
return false;
}
if (player.loseLife(lifeToPay, game, source, false) >= lifeToPay) {
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIFE_PAID, player.getId(), source, player.getId(), lifeToPay));
return true;
}
return false;
}
/**
* Generates source log name to insert into log messages
*
* @param game
* @param sourceId
* @param namePrefix if source object exists then that will be added before name
* @param namePostfix if source object exists then that will be added after name
* @param nonFoundText if source object not exists then it will be used
* @return
*/
public static String getSourceLogName(Game game, String namePrefix, UUID sourceId, String namePostfix, String nonFoundText) {
MageObject sourceObject = game.getObject(sourceId);
return (sourceObject == null ? nonFoundText : namePrefix + sourceObject.getLogName() + namePostfix);
}
public static String getSourceLogName(Game game, String namePrefix, Ability source, String namePostfix, String nonFoundText) {
return getSourceLogName(game, namePrefix, source == null ? null : source.getSourceId(), namePostfix, nonFoundText);
}
public static String getSourceLogName(Game game, Ability source) {
return CardUtil.getSourceLogName(game, " (source: ", source, ")", "");
}
public static String getSourceLogName(Game game, Ability source, UUID ignoreSourceId) {
if (ignoreSourceId != null && source != null && ignoreSourceId.equals(source.getSourceId())) {
return "";
}
return CardUtil.getSourceLogName(game, " (source: ", source, ")", "");
}
}