package magic.model; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Set; import magic.ai.ArtificialScoringSystem; import magic.data.CardDefinitions; import magic.data.CardProperty; import magic.model.condition.MagicCondition; import magic.model.event.MagicActivation; import magic.model.event.MagicActivationHints; import magic.model.event.MagicAdditionalCost; import magic.model.event.MagicCardEvent; import magic.model.event.MagicEvent; import magic.model.event.MagicEventSource; import magic.model.event.MagicHandCastActivation; import magic.model.event.MagicManaActivation; import magic.model.event.MagicPayManaCostEvent; import magic.model.event.MagicPermanentActivation; import magic.model.event.MagicPlayCardEvent; import magic.model.event.MagicTiming; import magic.model.mstatic.MagicCDA; import magic.model.mstatic.MagicStatic; import magic.model.trigger.AtBeginOfFirstMainPhaseTrigger; import magic.model.trigger.EntersBattlefieldTrigger; import magic.model.trigger.EntersWithCounterTrigger; import magic.model.trigger.MagicTrigger; import magic.model.trigger.ThisCycleTrigger; import magic.model.trigger.ThisDrawnTrigger; import magic.model.trigger.ThisPutIntoGraveyardTrigger; import magic.model.trigger.ThisSpellIsCastTrigger; import magic.ui.MagicCardImages; public class MagicCardDefinition implements MagicAbilityStore, IRenderableCard { /** * Comparator for sorting instances of {@code MagicCardDefinition} by distinct name. */ public static final Comparator SORT_BY_NAME = Comparator.comparing(MagicCardDefinition::getDistinctName); private static final List unsupportedStatuses = new ArrayList<>(); private static boolean isSorted = false; public static final MagicCardDefinition UNKNOWN = new MagicCardDefinition() { //definition for unknown cards @Override protected void initialize() { setName("Unknown"); setDistinctName("Unknown"); setToken(); setValue(1); addType(MagicType.Creature); setColors(""); setPowerToughness(1,1); setTiming(MagicTiming.Main); setIndex(1000000); } }; // name displayed in UI, may be repeated in tokens private String name; // name used for mapping and persistence, must be unique private String distinctName; private String imageURL; private Date imageUpdated; private int index=-1; private double value; private int removal; private int score=-1; // not initialized private MagicRarity rarity; private boolean valid = true; private boolean token; private boolean secondHalf; private boolean hidden; private boolean overlay; private boolean excludeManaOrCombat; private int typeFlags; private EnumSet cardType = EnumSet.noneOf(MagicType.class); private EnumSet subTypeFlags = EnumSet.noneOf(MagicSubType.class); private String subTypeText =""; private EnumSet abilityFlags = EnumSet.noneOf(MagicAbility.class); private int colorFlags = -1; private MagicManaCost cost = MagicManaCost.NONE; private String manaSourceText=""; private final int[] manaSource=new int[MagicColor.NR_COLORS]; private int power; private int toughness; private String powerToughnessText = ""; private int startingLoyalty; private String text = ""; private MagicStaticType staticType=MagicStaticType.None; private MagicTiming timing=MagicTiming.None; private MagicCardEvent cardEvent=MagicPlayCardEvent.create(); private final Collection> permActivations= new ArrayList<>(); private final Collection> morphActivations= new ArrayList<>(); private final LinkedList> handActivations = new LinkedList<>(); private final LinkedList> graveyardActivations = new LinkedList<>(); private final Collection CDAs = new ArrayList<>(); private final Collection> triggers = new ArrayList<>(); private final Collection statics= new ArrayList<>(); private final LinkedList etbTriggers = new LinkedList<>(); private final Collection spellIsCastTriggers = new ArrayList<>(); private final Collection cycleTriggers = new ArrayList<>(); private final Collection drawnTriggers = new ArrayList<>(); private final Collection putIntoGraveyardTriggers = new ArrayList<>(); private final Collection manaActivations= new ArrayList<>(); private final Collection costEventSources= new ArrayList<>(); private MagicCardDefinition flipCardDefinition; private MagicCardDefinition transformCardDefinition; private MagicCardDefinition splitCardDefinition; private String abilityProperty; private String requiresGroovy; private String effectProperty; private String flipCardName; private String transformCardName; private String[] meldCardNames; private String splitCardName; private String status; public MagicCardDefinition() { initialize(); } public static MagicCardDefinition token(final MagicObject obj, final MagicCardDefinitionInit init) { return token(obj.getCardDefinition(), init); } public static MagicCardDefinition token(final MagicCardDefinition template, final MagicCardDefinitionInit init) { final MagicCardDefinition cdef = new MagicCardDefinition(); cdef.setName(template.getName()); cdef.setDistinctName(template.getDistinctName()); cdef.setPowerToughness(template.getCardPower(), template.getCardToughness()); cdef.setPowerToughnessText(template.getPowerToughnessText()); cdef.setColorFlags(template.getColorFlags()); cdef.setSubTypes(template.genSubTypes()); cdef.setSubTypeText(template.getSubTypeText()); for (final MagicType t : template.getTypes()) { cdef.addType(t); } if (template.hasExcludeManaOrCombat()) { cdef.setExcludeManaOrCombat(); } cdef.manaSourceText = template.manaSourceText; cdef.setEvent(template.getCardEvent()); cdef.setText(template.getText()); cdef.setValue(template.getValue()); cdef.setStatus(template.getStatus()); cdef.setStartingLoyalty(template.getStartingLoyalty()); cdef.setAbilityProperty(template.abilityProperty); cdef.setRequiresGroovy(template.requiresGroovy); cdef.abilityFlags.addAll(template.abilityFlags); cdef.permActivations.addAll(template.permActivations); cdef.morphActivations.addAll(template.morphActivations); cdef.CDAs.addAll(template.CDAs); cdef.triggers.addAll(template.triggers); cdef.statics.addAll(template.statics); cdef.etbTriggers.addAll(template.etbTriggers); cdef.manaActivations.addAll(template.manaActivations); cdef.setToken(); init.initialize(cdef); cdef.validate(); return cdef; } public boolean canHaveAnyNumberInDeck() { return hasType(MagicType.Basic) || MagicDeckConstructionRule.isUnlimitedCard(name); } protected void initialize() {} public void setAbilityProperty(final String value) { abilityProperty = value; } public void setRequiresGroovy(final String value) { requiresGroovy = value; } public void setEffectProperty(final String value) { effectProperty = value; } public void setFlipCardName(final String value) { flipCardName = value; } public void setTransformCardName(final String value) { transformCardName = value; } public void setMeldCardNames(final String[] value) { meldCardNames = value; } public void setSplitCardName(final String value) { splitCardName = value; } public void setSecondHalf() { secondHalf = true; } @Override public boolean isSecondHalf() { return secondHalf; } public void setHidden() { hidden = true; } @Override public boolean isHidden() { return hidden; } public void setOverlay() { overlay = true; } public boolean isOverlay() { return overlay; } public boolean isPlayable() { return !overlay && !token && !hidden && !secondHalf; } public boolean isNonPlayable() { return !isPlayable(); } public synchronized void loadAbilities() { if (isPlayable() && (hasCost() || isLand()) && handActivations.isEmpty()) { add(new MagicHandCastActivation(this)); if (isLegendary() && isSorcery()) { add(MagicAdditionalCost.create(MagicCondition.LEGENDARY_SORCERY)); } } if (startingLoyalty > 0 && etbTriggers.isEmpty()) { add(new EntersWithCounterTrigger( MagicCounterType.Loyalty, startingLoyalty )); } if (isSaga() && etbTriggers.isEmpty()) { add(new EntersWithCounterTrigger(MagicCounterType.Lore, 1)); add(AtBeginOfFirstMainPhaseTrigger.Saga); } if (requiresGroovy != null) { CardProperty.LOAD_GROOVY_CODE.setProperty(this, requiresGroovy); requiresGroovy = null; } if (abilityProperty != null) { CardProperty.LOAD_ABILITY.setProperty(this, abilityProperty); abilityProperty = null; } if (effectProperty != null) { CardProperty.LOAD_EFFECT.setProperty(this, effectProperty); effectProperty = null; } if (getFlippedDefinition().isHidden()) { flipCardDefinition.loadAbilities(); } if (getTransformedDefinition().isHidden()) { transformCardDefinition.loadAbilities(); } if (getSplitDefinition().isSecondHalf()) { splitCardDefinition.loadAbilities(); } } public boolean isValid() { return valid; } @Override public boolean isInvalid() { return !valid; } public void setInvalid() { valid = false; } public void setImageUpdated(final Date d) { imageUpdated = d; } /** * Returns true if script file has a non-null {@code image_updated} property * whose value is a date that comes after the given date. */ public boolean isImageUpdatedAfter(final Date d) { return imageUpdated != null && imageUpdated.after(d); } /** * Returns the name of the card exactly as it appears on the printed card. *

* Note that in the case of token cards this means it may return the * same name (eg. five different Wurm tokens would all return "Wurm"). * * @see getDistinctName() */ @Override public String getName() { return name; } public void setName(final String name) { this.name = name; } /** * Returns a guaranteed distinct card name. *

* In most cases this will be the same as {@link getName()} but for tokens * of the same type (eg. Wurm) this will return a name that clearly identifies * the card (eg. 5/5 green Wurm creature token with trample). * */ public String getDistinctName() { return distinctName; } public void setDistinctName(String aName) { assert !name.equals(aName) || name == aName : "Same name but using two separate strings. Should reference same string for efficiency."; distinctName = aName; } /** * Returns the name of the card containing only ASCII characters. */ public String getAsciiName() { return CardDefinitions.getASCII(distinctName); } /** * Returns the name of the script/groovy file without extension */ public String getFilename() { return CardDefinitions.getCanonicalName(distinctName); } public void setIndex(final int index) { this.index=index; } public int getIndex() { return index; } @Override public String getImageName() { return token ? CardDefinitions.getCanonicalName(distinctName): distinctName.replaceAll("[<>:\"/\\\\|?*\\x00-\\x1F]", "_"); } public void setImageURL(final String imageURL) { this.imageURL = imageURL; } public String getImageURL() { return imageURL; } public boolean hasImageUrl() { return imageURL != null; } public String getCardTextName() { return getImageName(); } public void setValue(final double aValue) { value = aValue; } public double getValue() { return value; } public void setRemoval(final int removal) { this.removal=removal; } public int getRemoval() { return removal; } public int getScore() { if (score<0) { score=ArtificialScoringSystem.getCardDefinitionScore(this); } return score; } public int getFreeScore() { if (score<0) { score=ArtificialScoringSystem.getFreeCardDefinitionScore(this); } return score; } public void setRarity(final char c) { rarity = MagicRarity.getRarity(c); } public boolean isRarity(final MagicRarity r) { return rarity == r; } public int getRarity() { return rarity == null ? -1 : rarity.ordinal(); } public String getRarityString() { return rarity == null ? "" : rarity.getName(); } @Override public Character getRarityChar() { return rarity == null ? 'C' : rarity.getChar();//Return common for null rarity } public void setToken() { token=true; } @Override public boolean isToken() { return token; } int getTypeFlags() { return typeFlags; } public void addType(final MagicType type) { typeFlags |= type.getMask(); if (type == MagicType.Land) { if (colorFlags == -1) { // Lands default to colorless colorFlags = 0; } else { assert colorFlags != 0 : "redundant color declaration: " + colorFlags; } } cardType.add(type); } public Set getCardType() { return cardType; } @Override public boolean hasType(final MagicType type) { return (typeFlags&type.getMask())!=0; } public MagicCardDefinition getFlippedDefinition() { if (flipCardDefinition == null) { flipCardDefinition = isFlipCard() ? CardDefinitions.getMissingOrCard(flipCardName) : UNKNOWN; } return flipCardDefinition; } @Override public MagicCardDefinition getTransformedDefinition() { if (transformCardDefinition == null) { transformCardDefinition = isDoubleFaced() ? CardDefinitions.getMissingOrCard(transformCardName) : UNKNOWN; } return transformCardDefinition; } @Override public MagicCardDefinition getSplitDefinition() { if (splitCardDefinition == null) { splitCardDefinition = isSplitCard() ? CardDefinitions.getMissingOrCard(splitCardName) : UNKNOWN; } return splitCardDefinition; } @Override public MagicCardDefinition getCardDefinition() { return this; } public boolean isBasic() { return hasType(MagicType.Basic); } public boolean isEquipment() { return hasSubType(MagicSubType.Equipment); } public boolean isTribal() { return hasType(MagicType.Tribal); } public boolean isSnow() { return hasType(MagicType.Snow); } public boolean isWorld() { return hasType(MagicType.World); } public boolean isAura() { return isEnchantment() && hasSubType(MagicSubType.Aura); } public boolean isSpell() { return isInstant()||isSorcery(); } public boolean isPermanent() { return !isSpell(); } @Override public boolean isFlipCard() { return flipCardName != null; } @Override public boolean isDoubleFaced() { return transformCardName != null; } @Override public boolean isSplitCard() { return splitCardName != null; } public boolean hasMultipleAspects() { return isFlipCard() || isDoubleFaced(); } public String getLongTypeString() { if (isBasic()) { return (isSnow() ? "Basic Snow " : "Basic ") + getTypeString(); } if (isLegendary()) { return (isSnow() ? "Legendary Snow " : "Legendary ") + getTypeString(); } if (isSnow()) { return "Snow " + getTypeString(); } if (isWorld()) { return "World " + getTypeString(); } return getTypeString(); } public String getTypeString() { final StringBuilder sb = new StringBuilder(); MagicType.TYPE_ORDER.stream().filter(this::hasType).forEach(type -> sb.append(type).append(' ')); return sb.toString().trim(); } public boolean usesStack() { return !isLand(); } public void setSubTypes(final String[] subTypeNames) { subTypeFlags = MagicSubType.getSubTypes(subTypeNames); } private void setSubTypes(final EnumSet stf) { subTypeFlags = stf; } public void addSubType(final MagicSubType subType) { subTypeFlags.add(subType); } EnumSet genSubTypes() { return subTypeFlags.clone(); } @Override public EnumSet getSubTypes() { final EnumSet subTypes = genSubTypes(); applyCDASubType(null, null, subTypes); return subTypes; } public void applyCDASubType(final MagicGame game, final MagicPlayer player, final Set flags) { for (final MagicCDA lv : CDAs) { lv.getSubTypeFlags(game, player, flags); } } public String getSubTypeString() { final String brackets = getSubTypes().toString(); // [...,...] if (brackets.length() <= 2) { return ""; } return brackets.substring(1, brackets.length() - 1); } @Override public boolean hasSubType(final MagicSubType subType) { return getSubTypes().contains(subType); } private void setColorFlags(final int cf) { colorFlags = cf; } public void setColors(final String colors) { colorFlags = MagicColor.getFlags(colors); assert !hasCost() || colorFlags != cost.getColorFlags() : "redundant color declaration: " + colorFlags; } @Override public boolean hasColor(final MagicColor color) { return (colorFlags&color.getMask())!=0; } public boolean isColorless() { return colorFlags == 0; } @Override public int getColorFlags() { return colorFlags; } public int applyCDAColor(final MagicGame game, final MagicPlayer player, final int initColor) { int color = initColor; for (final MagicCDA lv : CDAs) { color =lv.getColorFlags(game, player, color); } return color; } public int getConvertedCost() { return cost.getConvertedCost(); } public int getConvertedCost(final int x) { return cost.getConvertedCost(x); } public boolean hasConvertedCost(final int c) { return getConvertedCost() == c; } public int getCostBucket() { switch (getConvertedCost()) { case 0: case 1: case 2: return 0; case 3: case 4: return 1; default: return 2; } } public boolean hasX() { return cost.hasX(); } public void setCost(final MagicManaCost aCost) { cost = aCost; if (colorFlags == -1) { // color defaults to follow mana cost colorFlags = cost.getColorFlags(); } else { assert colorFlags != cost.getColorFlags() : "redundant color declaration: " + colorFlags; } } public void validate() { //every card should have a timing hint if (!isToken() && getTiming() == MagicTiming.None) { throw new RuntimeException( getName() + " does not have a timing hint" ); } //check colorFlags is set if (colorFlags == -1) { throw new RuntimeException(name + "'s color is not set"); } //every Aura should have an MagicPlayAuraEvent if (isAura() && cardEvent == MagicPlayCardEvent.create()) { throw new RuntimeException(name + " does not have the enchant property"); } } @Override public MagicManaCost getCost() { return cost; } public boolean hasCost() { return cost != MagicManaCost.NONE; } public List getCostEvent(final MagicCard source) { return getCostEvent(source, cost); } public List getWithoutManaCostEvent(final MagicCard source) { return getCostEvent(source, MagicManaCost.ZERO); } private List getCostEvent(final MagicCard source, final MagicManaCost manaCost) { final List costEvent = new ArrayList<>(); if (manaCost != MagicManaCost.NONE) { costEvent.add(MagicPayManaCostEvent.Cast( source, manaCost )); } for (final MagicEventSource eventSource : costEventSources) { costEvent.add(eventSource.getEvent(source)); } return costEvent; } public boolean isPlayable(final MagicDeckProfile profile) { if (isLand()) { int source = 0; for (final MagicColor color : profile.getColors()) { source += getManaSource(color); } return source > 4; } else { return cost.getCostScore(profile) > 0; } } public void setManaSourceText(final String sourceText) { manaSourceText=sourceText; for (int index=0;index genAbilityFlags() { return abilityFlags.clone(); } @Override public boolean hasAbility(final MagicAbility ability) { return abilityFlags.contains(ability); } public void setText(final String text) { this.text = text; } @Override public String getText() { return text; } public String getFlattenedText() { return text.replace("\n", " "); } public void setStaticType(final MagicStaticType staticType) { this.staticType=staticType; } MagicStaticType getStaticType() { return staticType; } public void setTiming(final MagicTiming timing) { this.timing=timing; } public MagicTiming getTiming() { return timing; } @Override public void add(final MagicChangeCardDefinition mod) { mod.change(this); } public void setEvent(final MagicCardEvent aCardEvent) { assert cardEvent == MagicPlayCardEvent.create() : "Attempting to set two MagicCardEvents for " + this; cardEvent = aCardEvent; } public MagicCardEvent getCardEvent() { return cardEvent; } public MagicActivationHints getActivationHints() { return new MagicActivationHints(timing,true); } // cast card activation is the first element of handActivations public MagicActivation getCastActivation() { assert handActivations.size() >= 1 : this + " has no card activations"; return handActivations.getFirst(); } public Collection> getHandActivations() { return handActivations; } public Collection> getGraveyardActivations() { return graveyardActivations; } public void addCDA(final MagicCDA cda) { CDAs.add(cda); } public void addCostEvent(final MagicEventSource eventSource) { costEventSources.add(eventSource); } public void addTrigger(final ThisSpellIsCastTrigger trigger) { spellIsCastTriggers.add(trigger); } public void addTrigger(final ThisCycleTrigger trigger) { cycleTriggers.add(trigger); } public void addTrigger(final EntersBattlefieldTrigger trigger) { if (trigger.usesStack()) { etbTriggers.add(trigger); } else { etbTriggers.addFirst(trigger); } } public void addTrigger(final ThisPutIntoGraveyardTrigger trigger) { putIntoGraveyardTriggers.add(trigger); } public void addTrigger(final ThisDrawnTrigger trigger) { drawnTriggers.add(trigger); } public void addTrigger(final MagicTrigger trigger) { triggers.add(trigger); } public void addStatic(final MagicStatic mstatic) { statics.add(mstatic); } public Collection> getTriggers() { return triggers; } public Collection getStatics() { return statics; } public Collection getSpellIsCastTriggers() { return spellIsCastTriggers; } public Collection getCycleTriggers() { return cycleTriggers; } public Collection getETBTriggers() { return etbTriggers; } public Collection getPutIntoGraveyardTriggers() { return putIntoGraveyardTriggers; } public Collection getDrawnTriggers() { return drawnTriggers; } public void addAct(final MagicPermanentActivation activation) { permActivations.add(activation); } public void addMorphAct(final MagicPermanentActivation activation) { morphActivations.add(activation); } public void addHandAct(final MagicHandCastActivation activation) { handActivations.add(activation); } public void addGraveyardAct(final MagicHandCastActivation activation) { graveyardActivations.add(activation); } public void setHandAct(final MagicHandCastActivation activation) { assert handActivations.size() == 1 : "removing multiple (" + handActivations.size() + ") card activations"; handActivations.clear(); handActivations.add(activation); } public Collection> getActivations() { return permActivations; } public Collection> getMorphActivations() { return morphActivations; } public void addManaAct(final MagicManaActivation activation) { manaActivations.add(activation); } @Override public Collection getManaActivations() { return manaActivations; } public void setExcludeManaOrCombat() { excludeManaOrCombat=true; } public boolean hasExcludeManaOrCombat() { return excludeManaOrCombat; } private boolean subTypeHasText(final String s) { final MagicSubType[] subTypeValues = MagicSubType.values(); for (final MagicSubType subtype : subTypeValues) { if (subTypeFlags.contains(subtype) && subtype.toString().toLowerCase(Locale.ENGLISH).contains(s)) { return true; } } return false; } private boolean abilityHasText(final String s) { for (final MagicAbility ability : MagicAbility.values()) { if (hasAbility(ability) && ability.getName().toLowerCase(Locale.ENGLISH).contains(s)) { return true; } } return false; } public boolean hasText(String s) { s = s.toLowerCase(Locale.ENGLISH); return CardDefinitions.getASCII(distinctName).toLowerCase(Locale.ENGLISH).contains(s) || CardDefinitions.getASCII(name).toLowerCase(Locale.ENGLISH).contains(s) || subTypeHasText(s) || abilityHasText(s) || CardDefinitions.getASCII(getText()).toLowerCase(Locale.ENGLISH).contains(s); } @Override public String toString() { return getName(); } public static final Comparator NAME_COMPARATOR_ASC = Comparator.comparing(MagicCardDefinition::getName); public static final Comparator NAME_COMPARATOR_DESC = (cd1, cd2) -> cd2.getName().compareTo(cd1.getName()); public static final Comparator CONVERTED_COMPARATOR_DESC = (cd1, cd2) -> { final int cdif=cd1.getConvertedCost()-cd2.getConvertedCost(); return cdif != 0 ? cdif : cd1.getName().compareTo(cd2.getName()); }; public static final Comparator CONVERTED_COMPARATOR_ASC = (cd1, cd2) -> CONVERTED_COMPARATOR_DESC.compare(cd2, cd1); public static final Comparator TYPE_COMPARATOR_DESC = (cd1, cd2) -> { final int c = cd1.getTypeString().compareTo(cd2.getTypeString()); return c != 0 ? c : cd1.getLongTypeString().compareTo(cd2.getLongTypeString()); }; public static final Comparator TYPE_COMPARATOR_ASC = (cd1, cd2) -> TYPE_COMPARATOR_DESC.compare(cd2, cd1); public static final Comparator SUBTYPE_COMPARATOR_DESC = Comparator.comparing(MagicCardDefinition::getSubTypeString); public static final Comparator SUBTYPE_COMPARATOR_ASC = (cd1, cd2) -> SUBTYPE_COMPARATOR_DESC.compare(cd2, cd1); public static final Comparator RARITY_COMPARATOR_DESC = Comparator.comparingInt(MagicCardDefinition::getRarity); public static final Comparator RARITY_COMPARATOR_ASC = (cd1, cd2) -> RARITY_COMPARATOR_DESC.compare(cd2, cd1); public static final Comparator POWER_COMPARATOR_DESC = (cd1, cd2) -> { final int p1 = cd1.isCreature() ? cd1.getCardPower() : -100; final int p2 = cd2.isCreature() ? cd2.getCardPower() : -100; return p1 != p2 ? p1 - p2 : cd1.getName().compareTo(cd2.getName()); }; public static final Comparator POWER_COMPARATOR_ASC= (cd1, cd2) -> POWER_COMPARATOR_DESC.compare(cd2, cd1); public static final Comparator TOUGHNESS_COMPARATOR_DESC= (cd1, cd2) -> { final int t1 = cd1.isCreature() ? cd1.getCardToughness() : -100; final int t2 = cd2.isCreature() ? cd2.getCardToughness() : -100; return t1 != t2 ? t1 - t2 : cd1.getName().compareTo(cd2.getName()); }; public static final Comparator TOUGHNESS_COMPARATOR_ASC= (cd1, cd2) -> TOUGHNESS_COMPARATOR_DESC.compare(cd2, cd1); public boolean isImageFileMissing() { return MagicCardImages.isCardImageMissing(this); } public void setPowerToughnessText(String string) { powerToughnessText = string; } @Override public String getPowerToughnessText() { return powerToughnessText; } public void setSubTypeText(String string) { subTypeText = string.replaceAll("(\\w),(\\w)", "$1, $2");// Not automatically adding space unless space is there } @Override public String getSubTypeText() { return subTypeText; } public String getPowerLabel() { return isCreature() && powerToughnessText != null && !powerToughnessText.isEmpty() ? powerToughnessText.split("/")[0] : ""; } public String getToughnessLabel() { return isCreature() && powerToughnessText != null && !powerToughnessText.isEmpty() ? powerToughnessText.split("/")[1] : ""; } @Override public boolean hasText() { return !(getText().contains("NONE") || getText().length() <= 1); } public String getStatus() { return status; } public boolean hasStatus() { return status != null; } public boolean hasStatus(String aStatus) { return aStatus.equals(status); } public void setStatus(final String value) { int statusInt = unsupportedStatuses.indexOf(value); if (statusInt == -1) { unsupportedStatuses.add(value); status = value; } else { status = unsupportedStatuses.get(statusInt); } } public static String[] getUnsupportedStatuses() { if (!isSorted) { Collections.sort(unsupportedStatuses); isSorted = true; } return unsupportedStatuses.toArray(new String[0]); } }