Store cards by collectorNumber instead of artIndex; PaperCard flag support (#7240)

* Refactor - Unknown set code to constant

* Refactor - Support for multiple initial selections for getChoices

* Covert noSell and marked color identities into serializable flags

* Fix cards in deck not being converted to newer noSell format

* unused imports

* Fix NPE

* Cleanup card filter

* Remove 14-year-old check that shouldn't be possible anymore

* CRLF -> LF

---------

Co-authored-by: Jetz <Jetz722@gmail.com>
This commit is contained in:
Jetz72
2025-04-16 18:59:31 -04:00
committed by GitHub
parent c85214b9e3
commit 4714319204
63 changed files with 559 additions and 486 deletions

View File

@@ -158,7 +158,7 @@ public class ComputerUtilMana {
}
// Mana abilities on the same card
String shardMana = shard.toString().replaceAll("\\{", "").replaceAll("\\}", "");
String shardMana = shard.toShortString();
boolean payWithAb1 = ability1.getManaPart().mana(ability1).contains(shardMana);
boolean payWithAb2 = ability2.getManaPart().mana(ability2).contains(shardMana);

View File

@@ -42,7 +42,8 @@ import java.util.stream.Stream;
public final class CardDb implements ICardDatabase, IDeckGenPool {
public final static String foilSuffix = "+";
public final static char NameSetSeparator = '|';
public final static String colorIDPrefix = "#";
public final static String FlagPrefix = "#";
public static final String FlagSeparator = "\t";
private final String exlcudedCardName = "Concentrate";
private final String exlcudedCardSet = "DS0";
@@ -93,19 +94,19 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public int artIndex;
public boolean isFoil;
public String collectorNumber;
public Set<String> colorID;
public Map<String, String> flags;
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber) {
this(name, edition, artIndex, isFoil, collectorNumber, null);
}
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber, Set<String> colorID) {
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber, Map<String, String> flags) {
cardName = name;
this.edition = edition;
this.artIndex = artIndex;
this.isFoil = isFoil;
this.collectorNumber = collectorNumber;
this.colorID = colorID;
this.flags = flags;
}
public static boolean isFoilCardName(final String cardName){
@@ -120,7 +121,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
public static String compose(String cardName, String setCode) {
setCode = setCode != null ? setCode : "";
if(setCode == null || StringUtils.isBlank(setCode) || setCode.equals(CardEdition.UNKNOWN_CODE))
setCode = "";
cardName = cardName != null ? cardName : "";
if (cardName.indexOf(NameSetSeparator) != -1)
// If cardName is another RequestString, just get card name and forget about the rest.
@@ -134,14 +136,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return requestInfo + NameSetSeparator + artIndex;
}
public static String compose(String cardName, String setCode, int artIndex, Set<String> colorID) {
String requestInfo = compose(cardName, setCode);
artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
String cid = colorID == null ? "" : NameSetSeparator +
colorID.toString().replace("[", colorIDPrefix).replace(", ", colorIDPrefix).replace("]", "");
return requestInfo + NameSetSeparator + artIndex + cid;
}
public static String compose(String cardName, String setCode, String collectorNumber) {
String requestInfo = compose(cardName, setCode);
// CollectorNumber will be wrapped in square brackets
@@ -149,6 +143,34 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return requestInfo + NameSetSeparator + collectorNumber;
}
public static String compose(String cardName, String setCode, int artIndex, Map<String, String> flags) {
String requestInfo = compose(cardName, setCode);
artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
if(flags == null)
return requestInfo + NameSetSeparator + artIndex;
return requestInfo + NameSetSeparator + artIndex + getFlagSegment(flags);
}
public static String compose(String cardName, String setCode, String collectorNumber, Map<String, String> flags) {
String requestInfo = compose(cardName, setCode);
collectorNumber = preprocessCollectorNumber(collectorNumber);
if(flags == null || flags.isEmpty())
return requestInfo + NameSetSeparator + collectorNumber;
return requestInfo + NameSetSeparator + collectorNumber + getFlagSegment(flags);
}
public static String compose(PaperCard card) {
String name = compose(card.getName(), card.isFoil());
return compose(name, card.getEdition(), card.getCollectorNumber(), card.getMarkedFlags().toMap());
}
public static String compose(String cardName, String setCode, int artIndex, String collectorNumber) {
String requestInfo = compose(cardName, setCode, artIndex);
// CollectorNumber will be wrapped in square brackets
collectorNumber = preprocessCollectorNumber(collectorNumber);
return requestInfo + NameSetSeparator + collectorNumber;
}
private static String preprocessCollectorNumber(String collectorNumber) {
if (collectorNumber == null)
return "";
@@ -160,19 +182,21 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return collectorNumber;
}
public static String compose(String cardName, String setCode, int artIndex, String collectorNumber) {
String requestInfo = compose(cardName, setCode, artIndex);
// CollectorNumber will be wrapped in square brackets
collectorNumber = preprocessCollectorNumber(collectorNumber);
return requestInfo + NameSetSeparator + collectorNumber;
private static String getFlagSegment(Map<String, String> flags) {
if(flags == null)
return "";
String flagText = flags.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(FlagSeparator));
return NameSetSeparator + FlagPrefix + "{" + flagText + "}";
}
private static boolean isCollectorNumber(String s) {
return s.startsWith("[") && s.endsWith("]");
}
private static boolean isColorIDString(String s) {
return s.startsWith(colorIDPrefix);
private static boolean isFlagSegment(String s) {
return s.startsWith(FlagPrefix);
}
private static boolean isArtIndex(String s) {
@@ -201,44 +225,36 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return null;
String[] info = TextUtil.split(reqInfo, NameSetSeparator);
int setPos;
int artPos;
int cNrPos;
int clrPos;
if (info.length >= 4) { // name|set|artIndex|[collNr]
setPos = isSetCode(info[1]) ? 1 : -1;
artPos = isArtIndex(info[2]) ? 2 : -1;
cNrPos = isCollectorNumber(info[3]) ? 3 : -1;
int pos = cNrPos > 0 ? -1 : 3;
clrPos = pos > 0 ? isColorIDString(info[pos]) ? pos : -1 : -1;
} else if (info.length == 3) { // name|set|artIndex (or CollNr)
setPos = isSetCode(info[1]) ? 1 : -1;
artPos = isArtIndex(info[2]) ? 2 : -1;
cNrPos = isCollectorNumber(info[2]) ? 2 : -1;
int pos = cNrPos > 0 ? -1 : 2;
clrPos = pos > 0 ? isColorIDString(info[pos]) ? pos : -1 : -1;
} else if (info.length == 2) { // name|set (or artIndex, even if not possible via compose)
setPos = isSetCode(info[1]) ? 1 : -1;
artPos = isArtIndex(info[1]) ? 1 : -1;
cNrPos = -1;
clrPos = -1;
} else {
setPos = -1;
artPos = -1;
cNrPos = -1;
clrPos = -1;
}
int index = 1;
String cardName = info[0];
boolean isFoil = false;
int artIndex = IPaperCard.NO_ART_INDEX;
String setCode = null;
String collectorNumber = IPaperCard.NO_COLLECTOR_NUMBER;
Map<String, String> flags = null;
if (isFoilCardName(cardName)) {
cardName = cardName.substring(0, cardName.length() - foilSuffix.length());
isFoil = true;
}
int artIndex = artPos > 0 ? Integer.parseInt(info[artPos]) : IPaperCard.NO_ART_INDEX; // default: no art index
String collectorNumber = cNrPos > 0 ? info[cNrPos].substring(1, info[cNrPos].length() - 1) : IPaperCard.NO_COLLECTOR_NUMBER;
String setCode = setPos > 0 ? info[setPos] : null;
Set<String> colorID = clrPos > 0 ? Arrays.stream(info[clrPos].substring(1).split(colorIDPrefix)).collect(Collectors.toSet()) : null;
if (setCode != null && setCode.equals(CardEdition.UNKNOWN.getCode())) { // ???
if(info.length > index && isSetCode(info[index])) {
setCode = info[index];
index++;
}
if(info.length > index && isArtIndex(info[index])) {
artIndex = Integer.parseInt(info[index]);
index++;
}
if(info.length > index && isCollectorNumber(info[index])) {
collectorNumber = info[index].substring(1, info[index].length() - 1);
index++;
}
if (info.length > index && isFlagSegment(info[index])) {
String flagText = info[index].substring(FlagPrefix.length());
flags = parseRequestFlags(flagText);
}
if (CardEdition.UNKNOWN_CODE.equals(setCode)) { // ???
setCode = null;
}
if (setCode == null) {
@@ -253,7 +269,29 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
// finally, check whether any between artIndex and CollectorNumber has been set
if (collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER) && artIndex == IPaperCard.NO_ART_INDEX)
artIndex = IPaperCard.DEFAULT_ART_INDEX;
return new CardRequest(cardName, setCode, artIndex, isFoil, collectorNumber, colorID);
return new CardRequest(cardName, setCode, artIndex, isFoil, collectorNumber, flags);
}
private static Map<String, String> parseRequestFlags(String flagText) {
flagText = flagText.trim();
if(flagText.isEmpty())
return null;
if(!flagText.startsWith("{")) {
//Legacy form for marked colors. They'll be of the form "W#B#R"
Map<String, String> flags = new HashMap<>();
String normalizedColorString = ColorSet.fromNames(flagText.split(FlagPrefix)).toString();
flags.put("markedColors", String.join("", normalizedColorString));
return flags;
}
flagText = flagText.substring(1, flagText.length() - 1); //Trim the braces.
//List of flags, a series of "key=value" text broken up by tabs.
return Arrays.stream(flagText.split(FlagSeparator))
.map(f -> f.split("=", 2))
.filter(f -> f.length > 0)
.collect(Collectors.toMap(
entry -> entry[0],
entry -> entry.length > 1 ? entry[1] : "true" //If there's no '=' in the entry, treat it as a boolean flag.
));
}
}
@@ -406,7 +444,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
addCard(new PaperCard(cr, upcomingSet.getCode(), CardRarity.Unknown));
} else if (enableUnknownCards && !this.filtered.contains(cr.getName())) {
System.err.println("The card " + cr.getName() + " was not assigned to any set. Adding it to UNKNOWN set... to fix see res/editions/ folder. ");
addCard(new PaperCard(cr, CardEdition.UNKNOWN.getCode(), CardRarity.Special));
addCard(new PaperCard(cr, CardEdition.UNKNOWN_CODE, CardRarity.Special));
}
} else {
System.err.println("The custom card " + cr.getName() + " was not assigned to any set. Adding it to custom USER set, and will try to load custom art from USER edition.");
@@ -592,15 +630,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
@Override
public PaperCard getCard(final String cardName, String setCode, int artIndex, String collectorNumber) {
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, collectorNumber);
CardRequest request = CardRequest.fromString(reqInfo);
return tryGetCard(request);
}
@Override
public PaperCard getCard(final String cardName, String setCode, int artIndex, Set<String> colorID) {
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, colorID);
public PaperCard getCard(final String cardName, String setCode, int artIndex, Map<String, String> flags) {
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, flags);
CardRequest request = CardRequest.fromString(reqInfo);
return tryGetCard(request);
}
@@ -611,14 +642,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return null;
// 1. First off, try using all possible search parameters, to narrow down the actual cards looked for.
String reqEditionCode = request.edition;
if (reqEditionCode != null && reqEditionCode.length() > 0) {
if (reqEditionCode != null && !reqEditionCode.isEmpty()) {
// This get is robust even against expansion aliases (e.g. TE and TMP both valid for Tempest) -
// MOST of the extensions have two short codes, 141 out of 221 (so far)
// ALSO: Set Code are always UpperCase
CardEdition edition = editions.get(reqEditionCode.toUpperCase());
return this.getCardFromSet(request.cardName, edition, request.artIndex,
request.collectorNumber, request.isFoil, request.colorID);
PaperCard cardFromSet = this.getCardFromSet(request.cardName, edition, request.artIndex, request.collectorNumber, request.isFoil);
if(cardFromSet != null && request.flags != null)
cardFromSet = cardFromSet.copyWithFlags(request.flags);
return cardFromSet;
}
// 2. Card lookup in edition with specified filter didn't work.
@@ -661,11 +695,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override
public PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil) {
return getCardFromSet(cardName, edition, artIndex, collectorNumber, isFoil, null);
}
@Override
public PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil, Set<String> colorID) {
if (edition == null || cardName == null) // preview cards
return null; // No cards will be returned
@@ -674,18 +703,18 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
cardName = cardNameRequest.cardName;
isFoil = isFoil || cardNameRequest.isFoil;
List<PaperCard> candidates = getAllCards(cardName, c -> {
boolean artIndexFilter = true;
boolean collectorNumberFilter = true;
boolean setFilter = c.getEdition().equalsIgnoreCase(edition.getCode()) ||
c.getEdition().equalsIgnoreCase(edition.getCode2());
if (artIndex > 0)
artIndexFilter = (c.getArtIndex() == artIndex);
if ((collectorNumber != null) && (collectorNumber.length() > 0)
&& !(collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER)))
collectorNumberFilter = (c.getCollectorNumber().equals(collectorNumber));
return setFilter && artIndexFilter && collectorNumberFilter;
});
String code1 = edition.getCode(), code2 = edition.getCode2();
Predicate<PaperCard> filter = (c) -> {
String ed = c.getEdition();
return ed.equalsIgnoreCase(code1) || ed.equalsIgnoreCase(code2);
};
if (artIndex > 0)
filter = filter.and((c) -> artIndex == c.getArtIndex());
if (collectorNumber != null && !collectorNumber.isEmpty() && !collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER))
filter = filter.and((c) -> collectorNumber.equals(c.getCollectorNumber()));
List<PaperCard> candidates = getAllCards(cardName, filter);
if (candidates.isEmpty())
return null;
@@ -699,7 +728,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
while (!candidate.hasImage() && candidatesIterator.hasNext())
candidate = candidatesIterator.next();
candidate = candidate.hasImage() ? candidate : firstCandidate;
return isFoil ? candidate.getFoiled().getColorIDVersion(colorID) : candidate.getColorIDVersion(colorID);
return isFoil ? candidate.getFoiled() : candidate;
}
/*
@@ -742,11 +771,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, filter);
}
@Override
public PaperCard getCardFromEditions(final String cardInfo, final CardArtPreference artPreference, int artIndex, Set<String> colorID) {
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, null, false, null, colorID);
}
/*
* ===============================================
* 4. SPECIALISED CARD LOOKUP BASED ON
@@ -820,12 +844,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex,
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter){
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, releaseDate, releasedBeforeFlag, filter, null);
}
private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex,
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter, Set<String> colorID){
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter) {
if (cardInfo == null)
return null;
final CardRequest cr = CardRequest.fromString(cardInfo);
@@ -865,7 +884,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
for (PaperCard card : cards) {
String setCode = card.getEdition();
CardEdition ed;
if (setCode.equals(CardEdition.UNKNOWN.getCode()))
if (setCode.equals(CardEdition.UNKNOWN_CODE))
ed = CardEdition.UNKNOWN;
else
ed = editions.get(card.getEdition());
@@ -906,7 +925,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
candidate = candidate.hasImage() ? candidate : firstCandidate;
//If any, we're sure that at least one candidate is always returned despite it having any image
return cr.isFoil ? candidate.getFoiled().getColorIDVersion(colorID) : candidate.getColorIDVersion(colorID);
return cr.isFoil ? candidate.getFoiled() : candidate;
}
@Override
@@ -1126,29 +1145,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
.anyMatch(rarity::equals);
}
public StringBuilder appendCardToStringBuilder(PaperCard card, StringBuilder sb) {
final boolean hasBadSetInfo = card.getEdition().equals(CardEdition.UNKNOWN.getCode()) || StringUtils.isBlank(card.getEdition());
sb.append(card.getName());
if (card.isFoil()) {
sb.append(CardDb.foilSuffix);
}
if (!hasBadSetInfo) {
int artCount = getArtCount(card.getName(), card.getEdition(), card.getFunctionalVariant());
sb.append(CardDb.NameSetSeparator).append(card.getEdition());
if (artCount >= IPaperCard.DEFAULT_ART_INDEX) {
sb.append(CardDb.NameSetSeparator).append(card.getArtIndex()); // indexes start at 1 to match image file name conventions
}
if (card.getColorID() != null) {
sb.append(CardDb.NameSetSeparator);
for (String color : card.getColorID())
sb.append(CardDb.colorIDPrefix).append(color);
}
}
return sb;
}
public PaperCard createUnsupportedCard(String cardRequest) {
CardRequest request = CardRequest.fromString(cardRequest);
CardEdition cardEdition = CardEdition.UNKNOWN;
@@ -1250,7 +1246,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
}
if (paperCards.isEmpty()) {
paperCards.add(new PaperCard(rules, CardEdition.UNKNOWN.getCode(), CardRarity.Special));
paperCards.add(new PaperCard(rules, CardEdition.UNKNOWN_CODE, CardRarity.Special));
}
// 2. add them to db
for (PaperCard paperCard : paperCards) {

View File

@@ -262,7 +262,11 @@ public final class CardEdition implements Comparable<CardEdition> {
private final static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
public static final CardEdition UNKNOWN = new CardEdition("1990-01-01", "???", "??", Type.UNKNOWN, "Undefined", FoilType.NOT_SUPPORTED, new CardInSet[]{});
/**
* Equivalent to the set code of CardEdition.UNKNOWN
*/
public static final String UNKNOWN_CODE = "???";
public static final CardEdition UNKNOWN = new CardEdition("1990-01-01", UNKNOWN_CODE, "??", Type.UNKNOWN, "Undefined", FoilType.NOT_SUPPORTED, new CardInSet[]{});
private Date date;
private String code;
private String code2;

View File

@@ -396,6 +396,7 @@ public final class CardRules implements ICardCharacteristics {
}
public int getSetColorID() {
//Could someday generalize this to support other kinds of markings.
return setColorID;
}

View File

@@ -25,6 +25,8 @@ import forge.util.BinaryUtil;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* <p>CardColor class.</p>
@@ -291,14 +293,8 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
*/
@Override
public String toString() {
if (this.orderWeight == -1) {
return "n/a";
}
final String toReturn = MagicColor.toLongString(myColor);
if (toReturn.equals(MagicColor.Constant.COLORLESS) && myColor != 0) {
return "multi";
}
return toReturn;
final ManaCostShard[] orderedShards = getOrderedShards();
return Arrays.stream(orderedShards).map(ManaCostShard::toShortString).collect(Collectors.joining());
}
/**
@@ -376,6 +372,10 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
}
}
public Stream<MagicColor.Color> stream() {
return this.toEnumSet().stream();
}
//Get array of mana cost shards for color set in the proper order
public ManaCostShard[] getOrderedShards() {
return shardOrderLookup[myColor];

View File

@@ -5,43 +5,42 @@ import forge.item.PaperCard;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.Set;
/**
* Magic Cards Database.
* --------------------
* This interface defines the general API for Database Access and Cards' Lookup.
* <p>
* Methods for single Card's lookup currently support three alternative strategies:
* 1. [getCard]: Card search based on a single card's attributes
* (i.e. name, edition, art, collectorNumber)
* <p>
* 2. [getCardFromSet]: Card Lookup from a single Expansion set.
* Particularly useful in Deck Editors when a specific Set is specified.
* <p>
* 3. [getCardFromEditions]: Card search considering a predefined `SetPreference` policy and/or a specified Date
* when no expansion is specified for a card.
* This method is particularly useful for Re-prints whenever no specific
* Expansion is specified (e.g. in Deck Import) and a decision should be made
* on which card to pick. This methods allows to adopt a SetPreference selection
* policy to make this decision.
* <p>
* The API also includes methods to fetch Collection of Card (i.e. PaperCard instances):
* - all cards (no filter)
* - all unique cards (by name)
* - all prints of a single card
* - all cards from a single Expansion Set
* - all cards compliant with a filter condition (i.e. Predicate)
* <p>
* Finally, various utility methods are supported:
* - Get the foil version of a Card (if Any)
* - Get the Order Number of a Card in an Expansion Set
* - Get the number of Print/Arts for a card in a Set (useful for those exp. having multiple arts)
* */
public interface ICardDatabase extends Iterable<PaperCard> {
/**
* Magic Cards Database.
* --------------------
* This interface defines the general API for Database Access and Cards' Lookup.
*
* Methods for single Card's lookup currently support three alternative strategies:
* 1. [getCard]: Card search based on a single card's attributes
* (i.e. name, edition, art, collectorNumber)
*
* 2. [getCardFromSet]: Card Lookup from a single Expansion set.
* Particularly useful in Deck Editors when a specific Set is specified.
*
* 3. [getCardFromEditions]: Card search considering a predefined `SetPreference` policy and/or a specified Date
* when no expansion is specified for a card.
* This method is particularly useful for Re-prints whenever no specific
* Expansion is specified (e.g. in Deck Import) and a decision should be made
* on which card to pick. This methods allows to adopt a SetPreference selection
* policy to make this decision.
*
* The API also includes methods to fetch Collection of Card (i.e. PaperCard instances):
* - all cards (no filter)
* - all unique cards (by name)
* - all prints of a single card
* - all cards from a single Expansion Set
* - all cards compliant with a filter condition (i.e. Predicate)
*
* Finally, various utility methods are supported:
* - Get the foil version of a Card (if Any)
* - Get the Order Number of a Card in an Expansion Set
* - Get the number of Print/Arts for a card in a Set (useful for those exp. having multiple arts)
* */
/* SINGLE CARD RETRIEVAL METHODS
* ============================= */
// 1. Card Lookup by attributes
@@ -50,22 +49,19 @@ public interface ICardDatabase extends Iterable<PaperCard> {
PaperCard getCard(String cardName, String edition, int artIndex);
// [NEW Methods] Including the card CollectorNumber as criterion for DB lookup
PaperCard getCard(String cardName, String edition, String collectorNumber);
PaperCard getCard(String cardName, String edition, int artIndex, String collectorNumber);
PaperCard getCard(String cardName, String edition, int artIndex, Set<String> colorID);
PaperCard getCard(String cardName, String edition, int artIndex, Map<String, String> flags);
// 2. Card Lookup from a single Expansion Set
PaperCard getCardFromSet(String cardName, CardEdition edition, boolean isFoil); // NOT yet used, included for API symmetry
PaperCard getCardFromSet(String cardName, CardEdition edition, String collectorNumber, boolean isFoil);
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, boolean isFoil);
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil);
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil, Set<String> colorID);
// 3. Card lookup based on CardArtPreference Selection Policy
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference);
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, Predicate<PaperCard> filter);
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex);
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex, Predicate<PaperCard> filter);
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex, Set<String> colorID);
// 4. Specialised Card Lookup on CardArtPreference Selection and Release Date
PaperCard getCardFromEditionsReleasedBefore(String cardName, CardArtPreference artPreference, Date releaseDate);

View File

@@ -1,6 +1,7 @@
package forge.card;
import com.google.common.collect.ImmutableList;
import forge.deck.DeckRecognizer;
/**
* Holds byte values for each color magic has.
@@ -187,6 +188,12 @@ public final class MagicColor {
public String getName() {
return name;
}
public String getLocalizedName() {
//Should probably move some of this logic back here, or at least to a more general location.
return DeckRecognizer.getLocalisedMagicColorName(getName());
}
public byte getColormask() {
return colormask;
}

View File

@@ -94,6 +94,7 @@ public enum ManaCostShard {
/** The cmpc. */
private final float cmpc;
private final String stringValue;
private final String shortStringValue;
/** The image key. */
private final String imageKey;
@@ -125,6 +126,7 @@ public enum ManaCostShard {
this.cmc = this.getCMC();
this.cmpc = this.getCmpCost();
this.stringValue = "{" + sValue + "}";
this.shortStringValue = sValue;
this.imageKey = imgKey;
}
@@ -232,16 +234,21 @@ public enum ManaCostShard {
return ManaCostShard.valueOf(atoms);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
/**
* @return the string representation of this shard - e.g. "{W}" "{2/U}" "{G/P}"
*/
@Override
public final String toString() {
return this.stringValue;
}
/**
* @return The string representation of this shard without brackets - e.g. "W" "2/U" "G/P"
*/
public final String toShortString() {
return this.shortStringValue;
}
/**
* Gets the cmc.
*

View File

@@ -52,7 +52,7 @@ public class CardPool extends ItemPool<PaperCard> {
public void add(final String cardRequest, final int amount) {
CardDb.CardRequest request = CardDb.CardRequest.fromString(cardRequest);
this.add(CardDb.CardRequest.compose(request.cardName, request.isFoil), request.edition, request.artIndex, amount, false, request.colorID);
this.add(CardDb.CardRequest.compose(request.cardName, request.isFoil), request.edition, request.artIndex, amount, false, request.flags);
}
public void add(final String cardName, final String setCode) {
@@ -71,7 +71,7 @@ public class CardPool extends ItemPool<PaperCard> {
public void add(String cardName, String setCode, int artIndex, final int amount) {
this.add(cardName, setCode, artIndex, amount, false, null);
}
public void add(String cardName, String setCode, int artIndex, final int amount, boolean addAny, Set<String> colorID) {
public void add(String cardName, String setCode, int artIndex, final int amount, boolean addAny, Map<String, String> flags) {
Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases();
PaperCard paperCard = null;
String selectedDbName = "";
@@ -81,7 +81,7 @@ public class CardPool extends ItemPool<PaperCard> {
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
String dbName = entry.getKey();
CardDb db = entry.getValue();
paperCard = db.getCard(cardName, setCode, artIndex, colorID);
paperCard = db.getCard(cardName, setCode, artIndex, flags);
if (paperCard != null) {
selectedDbName = dbName;
break;
@@ -123,7 +123,7 @@ public class CardPool extends ItemPool<PaperCard> {
int cnt = artGroups[i - 1];
if (cnt <= 0)
continue;
PaperCard randomCard = cardDb.getCard(cardName, setCode, i, colorID);
PaperCard randomCard = cardDb.getCard(cardName, setCode, i, flags);
this.add(randomCard, cnt);
}
}
@@ -430,7 +430,6 @@ public class CardPool extends ItemPool<PaperCard> {
public String toCardList(String separator) {
List<Entry<PaperCard, Integer>> main2sort = Lists.newArrayList(this);
main2sort.sort(ItemPoolSorter.BY_NAME_THEN_SET);
final CardDb commonDb = StaticData.instance().getCommonCards();
StringBuilder sb = new StringBuilder();
boolean isFirst = true;
@@ -441,10 +440,8 @@ public class CardPool extends ItemPool<PaperCard> {
else
isFirst = false;
CardDb db = !e.getKey().getRules().isVariant() ? commonDb : StaticData.instance().getVariantCards();
sb.append(e.getValue()).append(" ");
db.appendCardToStringBuilder(e.getKey(), sb);
sb.append(CardDb.CardRequest.compose(e.getKey()));
}
return sb.toString();
}
@@ -463,20 +460,4 @@ public class CardPool extends ItemPool<PaperCard> {
}
return filteredPool;
}
/**
* Applies a predicate to this CardPool's cards.
* @param predicate the Predicate to apply to this CardPool
* @return a new CardPool made from this CardPool with only the cards that agree with the provided Predicate
*/
public CardPool getFilteredPoolWithCardsCount(Predicate<PaperCard> predicate) {
CardPool filteredPool = new CardPool();
for (Entry<PaperCard, Integer> entry : this.items.entrySet()) {
PaperCard pc = entry.getKey();
int count = entry.getValue();
if (predicate.test(pc))
filteredPool.add(pc, count);
}
return filteredPool;
}
}

View File

@@ -247,7 +247,7 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
Map<String, List<String>> referenceDeckLoadingMap;
if (deferredSections != null) {
this.validateDeferredSections();
this.normalizeDeferredSections();
referenceDeckLoadingMap = new HashMap<>(this.deferredSections);
} else
referenceDeckLoadingMap = new HashMap<>(loadedSections);
@@ -267,7 +267,7 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
continue;
final List<String> cardsInSection = s.getValue();
ArrayList<String> cardNamesWithNoEdition = getAllCardNamesWithNoSpecifiedEdition(cardsInSection);
if (cardNamesWithNoEdition.size() > 0) {
if (!cardNamesWithNoEdition.isEmpty()) {
includeCardsFromUnspecifiedSet = true;
if (smartCardArtSelection)
cardsWithNoEdition.put(sec, cardNamesWithNoEdition);
@@ -281,10 +281,10 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
optimiseCardArtSelectionInDeckSections(cardsWithNoEdition);
}
private void validateDeferredSections() {
private void normalizeDeferredSections() {
/*
Construct a temporary (DeckSection, CardPool) Maps, to be sanitised and finalised
before copying into `this.parts`. This sanitisation is applied because of the
before copying into `this.parts`. This sanitization is applied because of the
validation schema introduced in DeckSections.
*/
Map<String, List<String>> validatedSections = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
@@ -296,61 +296,33 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
}
final List<String> cardsInSection = s.getValue();
List<Pair<String, Integer>> originalCardRequests = CardPool.processCardList(cardsInSection);
CardPool pool = CardPool.fromCardList(cardsInSection);
if (pool.countDistinct() == 0)
continue; // pool empty, no card has been found!
// Filter pool by applying DeckSection Validation schema for Card Types (to avoid inconsistencies)
CardPool filteredPool = pool.getFilteredPoolWithCardsCount(deckSection::validate);
// Add all the cards from ValidPool anyway!
List<String> whiteList = validatedSections.getOrDefault(s.getKey(), null);
if (whiteList == null)
whiteList = new ArrayList<>();
for (Entry<PaperCard, Integer> entry : filteredPool) {
String poolRequest = getPoolRequest(entry, originalCardRequests);
whiteList.add(poolRequest);
List<String> validatedSection = validatedSections.computeIfAbsent(s.getKey(), (k) -> new ArrayList<>());
for (Entry<PaperCard, Integer> entry : pool) {
PaperCard card = entry.getKey();
String normalizedRequest = getPoolRequest(entry);
if(deckSection.validate(card))
validatedSection.add(normalizedRequest);
else {
// Card was in the wrong section. Move it to the right section.
DeckSection cardSection = DeckSection.matchingSection(card);
assert(cardSection.validate(card)); //Card doesn't fit in the matchingSection?
List<String> sectionCardList = validatedSections.computeIfAbsent(cardSection.name(), (k) -> new ArrayList<>());
sectionCardList.add(normalizedRequest);
}
}
validatedSections.put(s.getKey(), whiteList);
if (filteredPool.countDistinct() != pool.countDistinct()) {
CardPool blackList = pool.getFilteredPoolWithCardsCount(input -> !(deckSection.validate(input)));
for (Entry<PaperCard, Integer> entry : blackList) {
DeckSection cardSection = DeckSection.matchingSection(entry.getKey());
String poolRequest = getPoolRequest(entry, originalCardRequests);
List<String> sectionCardList = validatedSections.getOrDefault(cardSection.name(), null);
if (sectionCardList == null)
sectionCardList = new ArrayList<>();
sectionCardList.add(poolRequest);
validatedSections.put(cardSection.name(), sectionCardList);
} // end for blacklist
} // end if
} // end main for on deferredSections
// Overwrite deferredSections
this.deferredSections = validatedSections;
}
private String getPoolRequest(Entry<PaperCard, Integer> entry, List<Pair<String, Integer>> originalCardRequests) {
PaperCard card = entry.getKey();
private String getPoolRequest(Entry<PaperCard, Integer> entry) {
int amount = entry.getValue();
String poolCardRequest = CardDb.CardRequest.compose(
card.isFoil() ? CardDb.CardRequest.compose(card.getName(), true) : card.getName(),
card.getEdition(), card.getArtIndex(), card.getColorID());
String originalRequestCandidate = null;
for (Pair<String, Integer> originalRequest : originalCardRequests){
String cardRequest = originalRequest.getLeft();
if (!StringUtils.startsWithIgnoreCase(poolCardRequest, cardRequest))
continue;
originalRequestCandidate = cardRequest;
int cardAmount = originalRequest.getRight();
if (amount == cardAmount)
return String.format("%d %s", cardAmount, cardRequest);
}
// This is just in case, it should never happen as we're
if (originalRequestCandidate != null)
return String.format("%d %s", amount, originalRequestCandidate);
String poolCardRequest = CardDb.CardRequest.compose(entry.getKey());
return String.format("%d %s", amount, poolCardRequest);
}

View File

@@ -987,7 +987,7 @@ public class DeckRecognizer {
private static String getMagicColourLabel(MagicColor.Color magicColor) {
if (magicColor == null) // Multicolour
return String.format("%s {W}{U}{B}{R}{G}", getLocalisedMagicColorName("Multicolour"));
return String.format("%s %s", getLocalisedMagicColorName(magicColor.getName()), magicColor.getSymbol());
return String.format("%s %s", magicColor.getLocalizedName(), magicColor.getSymbol());
}
private static final HashMap<Integer, String> manaSymbolsMap = new HashMap<Integer, String>() {{
@@ -1006,8 +1006,8 @@ public class DeckRecognizer {
if (magicColor2 == null || magicColor2 == MagicColor.Color.COLORLESS
|| magicColor1 == MagicColor.Color.COLORLESS)
return String.format("%s // %s", getMagicColourLabel(magicColor1), getMagicColourLabel(magicColor2));
String localisedName1 = getLocalisedMagicColorName(magicColor1.getName());
String localisedName2 = getLocalisedMagicColorName(magicColor2.getName());
String localisedName1 = magicColor1.getLocalizedName();
String localisedName2 = magicColor2.getLocalizedName();
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask());
return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol);
}

View File

@@ -88,7 +88,7 @@ public enum DeckSection {
CardType t = card.getRules().getType();
// NOTE: Same rules applies to both Deck and Side, despite "Conspiracy cards" are allowed
// in the SideBoard (see Rule 313.2)
// Those will be matched later, in case (see `Deck::validateDeferredSections`)
// Those will be matched later, in case (see `Deck::normalizeDeferredSections`)
return !t.isConspiracy() && !t.isDungeon() && !t.isPhenomenon() && !t.isPlane() && !t.isScheme() && !t.isVanguard();
};

View File

@@ -61,6 +61,8 @@ public class DeckSerializer {
}
for(Entry<DeckSection, CardPool> s : d) {
if(s.getValue().isEmpty())
continue;
out.add(TextUtil.enclosedBracket(s.getKey().toString()));
out.add(s.getValue().toCardList(System.lineSeparator()));
}

View File

@@ -2,10 +2,10 @@ package forge.item;
import forge.card.CardRarity;
import forge.card.CardRules;
import forge.card.ColorSet;
import forge.card.ICardFace;
import java.io.Serializable;
import java.util.Set;
public interface IPaperCard extends InventoryItem, Serializable {
@@ -20,7 +20,7 @@ public interface IPaperCard extends InventoryItem, Serializable {
String getEdition();
String getCollectorNumber();
String getFunctionalVariant();
Set<String> getColorID();
ColorSet getMarkedColors();
int getArtIndex();
boolean isFoil();
boolean isToken();

View File

@@ -28,8 +28,10 @@ import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Optional;
import java.util.Set;
import java.io.ObjectStreamException;
import java.io.Serial;
import java.util.*;
import java.util.stream.Collectors;
/**
* A lightweight version of a card that matches real-world cards, to use outside of games (eg. inventory, decks, trade).
@@ -39,6 +41,7 @@ import java.util.Set;
* @author Forge
*/
public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet, IPaperCard {
@Serial
private static final long serialVersionUID = 2942081982620691205L;
// Reference to rules
@@ -55,16 +58,15 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
private String artist;
private final int artIndex;
private final boolean foil;
private Boolean hasImage;
private final boolean noSell;
private Set<String> colorID;
private String sortableName;
private final PaperCardFlags flags;
private final String sortableName;
private final String functionalVariant;
// Calculated fields are below:
private transient CardRarity rarity; // rarity is given in ctor when set is assigned
// Reference to a new instance of Self, but foiled!
private transient PaperCard foiledVersion, noSellVersion;
private transient PaperCard foiledVersion, noSellVersion, flaglessVersion;
private transient Boolean hasImage;
@Override
public String getName() {
@@ -89,8 +91,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
}
@Override
public Set<String> getColorID() {
return colorID;
public ColorSet getMarkedColors() {
return this.flags.markedColors;
}
@Override
@@ -147,32 +149,32 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return unFoiledVersion;
}
public PaperCard getNoSellVersion() {
if (this.noSell)
if (this.flags.noSellValue)
return this;
if (this.noSellVersion == null) {
this.noSellVersion = new PaperCard(this.rules, this.edition, this.rarity,
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, true);
}
if (this.noSellVersion == null)
this.noSellVersion = new PaperCard(this, this.flags.withNoSellValueFlag(true));
return this.noSellVersion;
}
public PaperCard getSellable() {
if (!this.noSell)
return this;
PaperCard sellable = new PaperCard(this.rules, this.edition, this.rarity,
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, false);
return sellable;
public PaperCard copyWithoutFlags() {
if(this.flaglessVersion == null) {
if(this.flags == PaperCardFlags.IDENTITY_FLAGS)
this.flaglessVersion = this;
else
this.flaglessVersion = new PaperCard(this, null);
}
return flaglessVersion;
}
public PaperCard getColorIDVersion(Set<String> colors) {
if (colors == null && this.colorID == null)
public PaperCard copyWithFlags(Map<String, String> flags) {
if(flags == null || flags.isEmpty())
return this.copyWithoutFlags();
return new PaperCard(this, new PaperCardFlags(flags));
}
public PaperCard copyWithMarkedColors(ColorSet colors) {
if(Objects.equals(colors, this.flags.markedColors))
return this;
if (this.colorID != null && this.colorID.equals(colors))
return this;
if (colors != null && colors.equals(this.colorID))
return this;
return new PaperCard(this.rules, this.edition, this.rarity,
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, this.noSell, colors);
return new PaperCard(this, this.flags.withMarkedColors(colors));
}
@Override
public String getItemType() {
@@ -180,8 +182,12 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return localizer.getMessage("lblCard");
}
public boolean isNoSell() {
return noSell;
public PaperCardFlags getMarkedFlags() {
return this.flags;
}
public boolean hasNoSellValue() {
return this.flags.noSellValue;
}
public boolean hasImage() {
return hasImage(false);
@@ -198,38 +204,41 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
IPaperCard.NO_COLLECTOR_NUMBER, IPaperCard.NO_ARTIST_NAME, IPaperCard.NO_FUNCTIONAL_VARIANT);
}
public PaperCard(final PaperCard copyFrom, final PaperCardFlags flags) {
this(copyFrom.rules, copyFrom.edition, copyFrom.rarity, copyFrom.artIndex, copyFrom.foil, copyFrom.collectorNumber,
copyFrom.artist, copyFrom.functionalVariant, flags);
this.flaglessVersion = copyFrom.flaglessVersion;
}
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
final int artIndex0, final boolean foil0, final String collectorNumber0,
final String artist0, final String functionalVariant) {
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, false);
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, null);
}
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
final int artIndex0, final boolean foil0, final String collectorNumber0,
final String artist0, final String functionalVariant, final boolean noSell0) {
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, noSell0, null);
}
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
final int artIndex0, final boolean foil0, final String collectorNumber0,
final String artist0, final String functionalVariant, final boolean noSell0, final Set<String> colorID0) {
if (rules0 == null || edition0 == null || rarity0 == null) {
protected PaperCard(final CardRules rules, final String edition, final CardRarity rarity,
final int artIndex, final boolean foil, final String collectorNumber,
final String artist, final String functionalVariant, final PaperCardFlags flags) {
if (rules == null || edition == null || rarity == null) {
throw new IllegalArgumentException("Cannot create card without rules, edition or rarity");
}
rules = rules0;
name = rules0.getName();
edition = edition0;
artIndex = Math.max(artIndex0, IPaperCard.DEFAULT_ART_INDEX);
foil = foil0;
rarity = rarity0;
artist = TextUtil.normalizeText(artist0);
collectorNumber = (collectorNumber0 != null) && (collectorNumber0.length() > 0) ? collectorNumber0 : IPaperCard.NO_COLLECTOR_NUMBER;
this.rules = rules;
name = rules.getName();
this.edition = edition;
this.artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
this.foil = foil;
this.rarity = rarity;
this.artist = TextUtil.normalizeText(artist);
this.collectorNumber = (collectorNumber != null && !collectorNumber.isEmpty()) ? collectorNumber : IPaperCard.NO_COLLECTOR_NUMBER;
// If the user changes the language this will make cards sort by the old language until they restart the game.
// This is a good tradeoff
sortableName = TextUtil.toSortableName(CardTranslation.getTranslatedName(rules0.getName()));
sortableName = TextUtil.toSortableName(CardTranslation.getTranslatedName(rules.getName()));
this.functionalVariant = functionalVariant != null ? functionalVariant : IPaperCard.NO_FUNCTIONAL_VARIANT;
noSell = noSell0;
colorID = colorID0;
if(flags == null || flags.equals(PaperCardFlags.IDENTITY_FLAGS))
this.flags = PaperCardFlags.IDENTITY_FLAGS;
else
this.flags = flags;
}
public static PaperCard FAKE_CARD = new PaperCard(CardRules.getUnsupportedCardNamed("Fake Card"), "Fake Edition", CardRarity.Common);
@@ -256,8 +265,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
}
if (!getCollectorNumber().equals(other.getCollectorNumber()))
return false;
// colorID can be NULL
if (getColorID() != other.getColorID())
if (!Objects.equals(flags, other.flags))
return false;
return (other.foil == foil) && (other.artIndex == artIndex);
}
@@ -269,13 +277,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
*/
@Override
public int hashCode() {
final int code = (name.hashCode() * 11) + (edition.hashCode() * 59) +
(artIndex * 2) + (getCollectorNumber().hashCode() * 383);
final int id = Optional.ofNullable(colorID).map(Set::hashCode).orElse(0);
if (foil) {
return code + id + 1;
}
return code + id;
return Objects.hash(name, edition, collectorNumber, artIndex, foil, flags);
}
// FIXME: Check
@@ -339,6 +341,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return Integer.compare(artIndex, o.getArtIndex());
}
@Serial
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
// default deserialization
ois.defaultReadObject();
@@ -354,6 +357,14 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
rarity = pc.getRarity();
}
@Serial
private Object readResolve() throws ObjectStreamException {
//If we deserialize an old PaperCard with no flags, reinitialize as a fresh copy to set default flags.
if(this.flags == null)
return new PaperCard(this, null);
return this;
}
@Override
public String getImageKey(boolean altState) {
String normalizedName = StringUtils.stripAccents(name);
@@ -493,4 +504,85 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public boolean isRebalanced() {
return StaticData.instance().isRebalanced(name);
}
/**
* Contains properties of a card which distinguish it from an otherwise identical copy of the card with the same
* name, edition, and collector number. Examples include permanent markings on the card, and flags for Adventure
* mode.
*/
public static class PaperCardFlags {
/**
* Chosen colors, for cards like Cryptic Spires.
*/
public final ColorSet markedColors;
/**
* Removes the sell value of the card in Adventure mode.
*/
public final boolean noSellValue;
//TODO: Could probably move foil here.
static final PaperCardFlags IDENTITY_FLAGS = new PaperCardFlags(Map.of());
protected PaperCardFlags(Map<String, String> flags) {
if(flags.containsKey("markedColors"))
markedColors = ColorSet.fromNames(flags.get("markedColors").split(""));
else
markedColors = null;
noSellValue = flags.containsKey("noSellValue");
}
//Copy constructor. There are some better ways to do this, and they should be explored once we have more than 4
//or 5 fields here. Just need to ensure it's impossible to accidentally change a field while the PaperCardFlags
//object is in use.
private PaperCardFlags(PaperCardFlags copyFrom, ColorSet markedColors, Boolean noSellValue) {
if(markedColors == null)
markedColors = copyFrom.markedColors;
else if(markedColors.isColorless())
markedColors = null;
this.markedColors = markedColors;
this.noSellValue = noSellValue != null ? noSellValue : copyFrom.noSellValue;
}
public PaperCardFlags withMarkedColors(ColorSet markedColors) {
if(markedColors == null)
markedColors = ColorSet.getNullColor();
return new PaperCardFlags(this, markedColors, null);
}
public PaperCardFlags withNoSellValueFlag(boolean noSellValue) {
return new PaperCardFlags(this, null, noSellValue);
}
private Map<String, String> asMap;
public Map<String, String> toMap() {
if(asMap != null)
return asMap;
Map<String, String> out = new HashMap<>();
if(markedColors != null && !markedColors.isColorless())
out.put("markedColors", markedColors.toString());
if(noSellValue)
out.put("noSellValue", "true");
asMap = out;
return out;
}
@Override
public String toString() {
return this.toMap().entrySet().stream()
.map((e) -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("\t"));
}
@Override
public boolean equals(Object o) {
if (!(o instanceof PaperCardFlags that)) return false;
return noSellValue == that.noSellValue && Objects.equals(markedColors, that.markedColors);
}
@Override
public int hashCode() {
return Objects.hash(markedColors, noSellValue);
}
}
}

View File

@@ -7,7 +7,6 @@ import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Set;
public class PaperToken implements InventoryItemFromSet, IPaperCard {
private static final long serialVersionUID = 1L;
@@ -153,7 +152,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
}
@Override
public Set<String> getColorID() {
public ColorSet getMarkedColors() {
return null;
}

View File

@@ -269,6 +269,13 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
// need not set out-of-sync: either remove did set, or nothing was removed
}
public void removeIf(Predicate<T> test) {
for (final T item : items.keySet()) {
if (test.test(item))
remove(item);
}
}
public void clear() {
items.clear();
}

View File

@@ -22,6 +22,7 @@ import forge.GameCommand;
import forge.StaticData;
import forge.card.CardStateName;
import forge.card.CardType.Supertype;
import forge.card.ColorSet;
import forge.card.GamePieceType;
import forge.card.MagicColor;
import forge.deck.DeckSection;
@@ -541,8 +542,8 @@ public class GameAction {
game.addLeftGraveyardThisTurn(lastKnownInfo);
}
if (c.hasChosenColorSpire()) {
copied.setChosenColorID(ImmutableSet.copyOf(c.getChosenColorID()));
if (c.hasMarkedColor()) {
copied.setMarkedColors(c.getMarkedColors());
}
copied.updateStateForView();
@@ -2414,15 +2415,14 @@ public class GameAction {
for (Card c : spires) {
// TODO: only do this for the AI, for the player part, get the encoded color from the deck file and pass
// it to either player or the papercard object so it feels like rule based for the player side..
if (!c.hasChosenColorSpire()) {
if (!c.hasMarkedColor()) {
if (takesAction.isAI()) {
List<String> colorChoices = new ArrayList<>(MagicColor.Constant.ONLY_COLORS);
String prompt = CardTranslation.getTranslatedName(c.getName()) + ": " +
Localizer.getInstance().getMessage("lblChooseNColors", Lang.getNumeral(2));
SpellAbility sa = new SpellAbility.EmptySa(ApiType.ChooseColor, c, takesAction);
sa.putParam("AILogic", "MostProminentInComputerDeck");
Set<String> chosenColors = new HashSet<>(takesAction.getController().chooseColors(prompt, sa, 2, 2, colorChoices));
c.setChosenColorID(chosenColors);
ColorSet chosenColors = ColorSet.fromNames(takesAction.getController().chooseColors(prompt, sa, 2, 2, MagicColor.Constant.ONLY_COLORS));
c.setMarkedColors(chosenColors);
}
}
}

View File

@@ -200,7 +200,7 @@ public class CloneEffect extends SpellAbilityEffect {
tgtCard.addRemembered(cardToCopy);
}
// spire
tgtCard.setChosenColorID(cardToCopy.getChosenColorID());
tgtCard.setMarkedColors(cardToCopy.getMarkedColors());
game.fireEvent(new GameEventCardStatsChanged(tgtCard));
}

View File

@@ -314,7 +314,7 @@ public class CopyPermanentEffect extends TokenEffectBase {
}
}
// spire
copy.setChosenColorID(original.getChosenColorID());
copy.setMarkedColors(original.getMarkedColors());
copy.setTokenSpawningAbility(sa);
copy.setGamePieceType(GamePieceType.TOKEN);

View File

@@ -296,7 +296,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
private String chosenType2 = "";
private List<String> notedTypes = new ArrayList<>();
private List<String> chosenColors;
private Set<String> chosenColorID;
private ColorSet markedColor;
private List<String> chosenName = new ArrayList<>();
private Integer chosenNumber;
private Player chosenPlayer;
@@ -409,7 +409,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
view.updateClassLevel(this);
view.updateDraftAction(this);
if (paperCard != null)
setChosenColorID(paperCard.getColorID());
setMarkedColors(paperCard.getMarkedColors());
}
public int getHiddenId() {
@@ -2231,18 +2231,18 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
public boolean hasChosenColor(String s) {
return chosenColors != null && chosenColors.contains(s);
}
public final Set<String> getChosenColorID() {
if (chosenColorID == null) {
return Sets.newHashSet();
public final ColorSet getMarkedColors() {
if (markedColor == null) {
return ColorSet.getNullColor();
}
return chosenColorID;
return markedColor;
}
public final void setChosenColorID(final Set<String> s) {
chosenColorID = s;
view.updateChosenColorID(this);
public final void setMarkedColors(final ColorSet s) {
markedColor = s;
view.updateMarkedColors(this);
}
public boolean hasChosenColorSpire() {
return chosenColorID != null && !chosenColorID.isEmpty();
public boolean hasMarkedColor() {
return markedColor != null && !markedColor.isColorless();
}
public final Card getChosenCard() {
return getChosenCards().getFirst();

View File

@@ -77,7 +77,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
private KeywordCollection cachedKeywords = new KeywordCollection();
private CardRarity rarity = CardRarity.Unknown;
private String setCode = CardEdition.UNKNOWN.getCode();
private String setCode = CardEdition.UNKNOWN_CODE;
private final CardStateView view;
private final Card card;

View File

@@ -433,11 +433,11 @@ public class CardView extends GameEntityView {
void updateChosenColors(Card c) {
set(TrackableProperty.ChosenColors, c.getChosenColors());
}
public Set<String> getChosenColorID() {
return get(TrackableProperty.ChosenColorID);
public ColorSet getMarkedColors() {
return get(TrackableProperty.MarkedColors);
}
void updateChosenColorID(Card c) {
set(TrackableProperty.ChosenColorID, c.getChosenColorID());
void updateMarkedColors(Card c) {
set(TrackableProperty.MarkedColors, c.getMarkedColors());
}
public FCollectionView<CardView> getMergedCardsCollection() {
return get(TrackableProperty.MergedCardsCollection);

View File

@@ -703,10 +703,10 @@ public class AbilityManaPart implements java.io.Serializable {
return "";
}
Card card = sa.getHostCard();
if (card != null && card.hasChosenColorSpire()) {
if (card != null && card.hasMarkedColor()) {
StringBuilder values = new StringBuilder();
for (String s : card.getChosenColorID()) {
values.append(MagicColor.toShortString(MagicColor.fromName(s))).append(" ");
for (byte c : card.getMarkedColors()) {
values.append(MagicColor.toShortString(c)).append(" ");
}
return values.toString();
}

View File

@@ -67,7 +67,6 @@ public enum TrackableProperty {
ChosenType2(TrackableTypes.StringType),
NotedTypes(TrackableTypes.StringListType),
ChosenColors(TrackableTypes.StringListType),
ChosenColorID(TrackableTypes.StringSetType),
ChosenCards(TrackableTypes.CardViewCollectionType),
ChosenNumber(TrackableTypes.StringType),
StoredRolls(TrackableTypes.StringListType),
@@ -102,6 +101,7 @@ public enum TrackableProperty {
NeedsTransformAnimation(TrackableTypes.BooleanType, FreezeMode.IgnoresFreeze),
NeedsUntapAnimation(TrackableTypes.BooleanType, FreezeMode.IgnoresFreeze),
NeedsTapAnimation(TrackableTypes.BooleanType, FreezeMode.IgnoresFreeze),
MarkedColors(TrackableTypes.ColorSetType),
ImprintedCards(TrackableTypes.CardViewCollectionType),
ExiledCards(TrackableTypes.CardViewCollectionType),

View File

@@ -182,7 +182,7 @@ public class GuiDesktop implements IGuiBase {
}
@Override
public <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final T selected, final Function<T, String> display) {
public <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final Collection<T> selected, final Function<T, String> display) {
/*if ((choices != null && !choices.isEmpty() && choices.iterator().next() instanceof GameObject) || selected instanceof GameObject) {
System.err.println("Warning: GameObject passed to GUI! Printing stack trace.");
Thread.dumpStack();

View File

@@ -214,7 +214,7 @@ public class CardDetailPanel extends SkinnedPanel {
set = state.getSetCode();
rarity = state.getRarity();
} else {
set = CardEdition.UNKNOWN.getCode();
set = CardEdition.UNKNOWN_CODE;
rarity = CardRarity.Unknown;
}
setInfoLabel.setText(set);

View File

@@ -134,10 +134,10 @@ public class GuiChoose {
return getChoices(message, min, max, choices, null, null);
}
public static <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final T selected, final Function<T, String> display) {
public static <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final Collection<T> selected, final Function<T, String> display) {
return getChoices(message, min, max, choices, selected, display, null);
}
public static <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final T selected, final Function<T, String> display, final CMatchUI matchUI) {
public static <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final Collection<T> selected, final Function<T, String> display, final CMatchUI matchUI) {
if (choices == null || choices.isEmpty()) {
if (min == 0) {
return new ArrayList<>();

View File

@@ -152,16 +152,13 @@ public class ListChooser<T> {
/** @return boolean */
public boolean show() {
return show(list.get(0));
return show(null);
}
/**
* Shows the dialog and returns after the dialog was closed.
*
* @param index0 index to select when shown
* @return a boolean.
*/
public boolean show(final T item) {
public boolean show(final Collection<T> item) {
if (this.called) {
throw new IllegalStateException("Already shown");
}
@@ -169,8 +166,12 @@ public class ListChooser<T> {
do {
//invoke later so selected item not set until dialog open
SwingUtilities.invokeLater(() -> {
if (list.contains(item)) {
lstChoices.setSelectedValue(item, true);
if (item != null) {
int[] indices = item.stream()
.mapToInt(list::indexOf)
.filter(i -> i >= 0)
.toArray();
lstChoices.setSelectedIndices(indices);
}
else {
lstChoices.setSelectedIndex(0);

View File

@@ -20,16 +20,15 @@ package forge.screens.deckeditor.controllers;
import java.awt.Toolkit;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import javax.swing.JMenu;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.deck.CardPool;
import forge.deck.Deck;
@@ -582,9 +581,9 @@ public abstract class ACEditorBase<TItem extends InventoryItem, TModel extends D
int val;
if ((val = existingCard.getRules().getSetColorID()) > 0) {
GuiUtils.addMenuItem(menu, label, null, () -> {
Set<String> colors = new HashSet<>(GuiChoose.getChoices(localizer.getMessage("lblChooseNColors", Lang.getNumeral(val)), val, val, MagicColor.Constant.ONLY_COLORS));
List<String> colors = GuiChoose.getChoices(localizer.getMessage("lblChooseNColors", Lang.getNumeral(val)), val, val, MagicColor.Constant.ONLY_COLORS);
// make an updated version
PaperCard updated = existingCard.getColorIDVersion(colors);
PaperCard updated = existingCard.copyWithMarkedColors(ColorSet.fromNames(colors));
// remove *quantity* instances of existing card
CDeckEditorUI.SINGLETON_INSTANCE.removeSelectedCards(false, 1);
// add *quantity* into the deck and set them as selected

View File

@@ -24,7 +24,6 @@ import java.awt.image.BufferedImage;
import java.util.*;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
@@ -1072,7 +1071,7 @@ public final class CMatchUI
}
@Override
public <T> List<T> getChoices(final String message, final int min, final int max, final List<T> choices, final T selected, final Function<T, String> display) {
public <T> List<T> getChoices(final String message, final int min, final int max, final List<T> choices, final List<T> selected, final Function<T, String> display) {
/*if ((choices != null && !choices.isEmpty() && choices.iterator().next() instanceof GameObject) || selected instanceof GameObject) {
System.err.println("Warning: GameObject passed to GUI! Printing stack trace.");
Thread.dumpStack();

View File

@@ -277,21 +277,6 @@ public class CardDbCardMockTestCase extends CardMockTestCase {
}
}
@Test
public void testGetCardByNameSetArtIndexAndCollectorNumber() {
// MultiArt Card
PaperCard card;
for (int i = 0; i < 4; i++) {
int artIndex = i + 1;
card = this.cardDb.getCard(cardNameHymnToTourach, editionHymnToTourach, artIndex,
collectorNumbersHymnToTourach[i]);
assertEquals(card.getName(), cardNameHymnToTourach);
assertEquals(card.getEdition(), editionHymnToTourach);
assertEquals(card.getCollectorNumber(), collectorNumbersHymnToTourach[i]);
assertEquals(card.getArtIndex(), artIndex);
}
}
@Test
public void testNullIsReturnedWithWrongInfo() {
String wrongEditionCode = "M11";
@@ -307,11 +292,6 @@ public class CardDbCardMockTestCase extends CardMockTestCase {
// Wrong collector number
card = this.cardDb.getCard(cardNameShivanDragon, editionShivanDragon, "wrongCN");
assertNull(card);
// wrong artIndex or collector number in getCard Full Info
card = this.cardDb.getCard(cardNameShivanDragon, editionShivanDragon, 3, collNrShivanDragon);
assertNull(card);
card = this.cardDb.getCard(cardNameShivanDragon, editionShivanDragon, 1, "wrongCN");
assertNull(card);
}
@Test
@@ -2105,7 +2085,7 @@ public class CardDbCardMockTestCase extends CardMockTestCase {
public void testGetCardFromUnknownSet() {
String unknownCardName = "Unknown Card Name";
PaperCard unknownCard = new PaperCard(CardRules.getUnsupportedCardNamed(unknownCardName),
CardEdition.UNKNOWN.getCode(), CardRarity.Unknown);
CardEdition.UNKNOWN_CODE, CardRarity.Unknown);
this.cardDb.addCard(unknownCard);
assertTrue(this.cardDb.getAllCards().contains(unknownCard));
assertNotNull(this.cardDb.getAllCards(unknownCardName));
@@ -2114,7 +2094,7 @@ public class CardDbCardMockTestCase extends CardMockTestCase {
PaperCard retrievedPaperCard = this.cardDb.getCard(unknownCardName);
assertNotNull(retrievedPaperCard);
assertEquals(retrievedPaperCard.getName(), unknownCardName);
assertEquals(retrievedPaperCard.getEdition(), CardEdition.UNKNOWN.getCode());
assertEquals(retrievedPaperCard.getEdition(), CardEdition.UNKNOWN_CODE);
}
@Test

View File

@@ -197,7 +197,7 @@ public class GuiMobile implements IGuiBase {
}
@Override
public <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final T selected, final Function<T, String> display) {
public <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final Collection<T> selected, final Function<T, String> display) {
return new WaitCallback<List<T>>() {
@Override
public void run() {

View File

@@ -120,7 +120,6 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
statistic.clear();
newCards.clear();
autoSellCards.clear();
noSellCards.clear();
AdventureEventController.clear();
AdventureQuestController.clear();
}
@@ -134,7 +133,6 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
private final ItemPool<PaperCard> newCards = new ItemPool<>(PaperCard.class);
public final ItemPool<PaperCard> autoSellCards = new ItemPool<>(PaperCard.class);
public final ItemPool<PaperCard> noSellCards = new ItemPool<>(PaperCard.class);
public void create(String n, Deck startingDeck, boolean male, int race, int avatar, boolean isFantasy, boolean isUsingCustomDeck, DifficultyData difficultyData) {
clear();
@@ -471,10 +469,43 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
}
}
if (data.containsKey("noSellCards")) {
//Legacy list of unsellable cards. Now done via CardRequest flags. Convert the corresponding cards.
PaperCard[] items = (PaperCard[]) data.readObject("noSellCards");
for (PaperCard item : items) {
if (item != null)
noSellCards.add(item.getNoSellVersion());
CardPool noSellPool = new CardPool();
noSellPool.addAllFlat(List.of(items));
for (Map.Entry<PaperCard, Integer> noSellEntry : noSellPool) {
PaperCard item = noSellEntry.getKey();
if (item == null)
continue;
int totalCopies = cards.count(item);
int noSellCopies = Math.min(noSellEntry.getValue(), totalCopies);
if (!cards.remove(item, noSellCopies)) {
System.err.printf("Failed to update noSellValue flag - %s%n", item);
continue;
}
int remainingSellableCopies = totalCopies - noSellCopies;
PaperCard noSellVersion = item.getNoSellVersion();
cards.add(noSellVersion, noSellCopies);
System.out.printf("Converted legacy noSellCards item - %s (%d / %d copies)%n", item, noSellCopies, totalCopies);
//Also go through their decks and update cards there.
for (Deck deck : decks) {
int inUse = 0;
for (Map.Entry<DeckSection, CardPool> section : deck) {
CardPool pool = section.getValue();
inUse += pool.count(item);
if(inUse > remainingSellableCopies) {
int toConvert = inUse - remainingSellableCopies;
pool.remove(item, toConvert);
pool.add(noSellVersion, toConvert);
System.out.printf("- Converted %d copies in deck - %s/%s%n", toConvert, deck.getName(), section.getKey());
}
}
}
}
}
if (data.containsKey("autoSellCards")) {
@@ -582,7 +613,6 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
data.storeObject("newCards", newCards.toFlatList().toArray(new PaperCard[0]));
data.storeObject("autoSellCards", autoSellCards.toFlatList().toArray(new PaperCard[0]));
data.storeObject("noSellCards", noSellCards.toFlatList().toArray(new PaperCard[0]));
return data;
}
@@ -636,11 +666,6 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
if (reward.isAutoSell()) {
autoSellCards.add(reward.getCard());
refreshEditor();
} else if (reward.isNoSell()) {
if (reward.getCard() != null) {
noSellCards.add(reward.getCard().getNoSellVersion());
refreshEditor();
}
}
break;
case Gold:
@@ -908,8 +933,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
}
public int cardSellPrice(PaperCard card) {
int valuable = cards.count(card) - noSellCards.count(card);
if (valuable == 0) {
if (card.hasNoSellValue()) {
return 0;
}
@@ -920,23 +944,13 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
}
public int sellCard(PaperCard card, Integer result, boolean addGold) {
// When selling cards, always try to sell cards worth something before selling cards that aren't worth anything
if (result == null || result < 1) return 0;
if (result == null || result < 1)
return 0;
float earned = 0;
int valuableCount = cards.count(card) - noSellCards.count(card);
int noValueToSell = result - valuableCount;
int amountValuableToSell = Math.min(result, valuableCount);
if (amountValuableToSell > 0) {
earned += cardSellPrice(card) * amountValuableToSell;
cards.remove(card, amountValuableToSell);
}
if (noValueToSell > 0) {
cards.remove(card, noValueToSell);
noSellCards.remove(card, noValueToSell);
}
int amountToSell = Math.min(result, cards.count(card));
if(!cards.remove(card, amountToSell))
return 0; //Failed to sell?
float earned = cardSellPrice(card) * amountToSell;
if (addGold) {
addGold((int) earned);
@@ -1198,10 +1212,6 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
return autoSellCards;
}
public ItemPool<PaperCard> getNoSellCards() {
return noSellCards;
}
public ItemPool<PaperCard> getSellableCards() {
ItemPool<PaperCard> sellableCards = new ItemPool<>(PaperCard.class);
sellableCards.addAllFlat(cards.toFlatList());
@@ -1236,7 +1246,6 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
collectionCards.addAll(cards);
if (!allCards) {
collectionCards.removeAll(autoSellCards);
collectionCards.removeAll(noSellCards);
}
return collectionCards;

View File

@@ -211,14 +211,13 @@ public class AdventureDeckEditor extends TabPageScreen<AdventureDeckEditor> {
});
}
int noSellCount = Current.player().noSellCards.count(card);
int autoSellCount = Current.player().autoSellCards.count(card);
int sellableCount = Current.player().getSellableCards().count(card);
if (noSellCount > 0) {
FMenuItem unsellableCount = new FMenuItem(Forge.getLocalizer().getMessage("lblUnsellableCount", noSellCount), null, null);
unsellableCount.setEnabled(false);
menu.addItem(unsellableCount);
if (card.hasNoSellValue()) {
FMenuItem unsellableIndicator = new FMenuItem(Forge.getLocalizer().getMessage("lblUnsellable"), null, null);
unsellableIndicator.setEnabled(false);
menu.addItem(unsellableIndicator);
}
if (sellableCount > 0) {
@@ -248,12 +247,13 @@ public class AdventureDeckEditor extends TabPageScreen<AdventureDeckEditor> {
if (showCollectionCards) {
collectionPool.addAllFlat(AdventurePlayer.current().getCollectionCards(false).toFlatList());
}
if (showNoSellCards) {
collectionPool.addAllFlat(AdventurePlayer.current().getNoSellCards().toFlatList());
collectionPool.removeAllFlat(cardsInUse.toFlatList());
} else {
cardsInUse.removeAllFlat(AdventurePlayer.current().getNoSellCards().toFlatList());
collectionPool.removeAllFlat(cardsInUse.toFlatList());
else if(showNoSellCards) {
collectionPool.addAll(AdventurePlayer.current().getCollectionCards(false).getFilteredPool(PaperCard::hasNoSellValue));
}
collectionPool.removeAllFlat(cardsInUse.toFlatList());
if (!showNoSellCards) {
collectionPool.removeIf(PaperCard::hasNoSellValue);
}
if (showAutoSellCards) {
collectionPool.addAllFlat(AdventurePlayer.current().getAutoSellCards().toFlatList());
@@ -1120,12 +1120,13 @@ public class AdventureDeckEditor extends TabPageScreen<AdventureDeckEditor> {
if (showCollectionCards) {
adventurePool.addAll(AdventurePlayer.current().getCollectionCards(false));
}
if (showNoSellCards) {
adventurePool.addAll(AdventurePlayer.current().getNoSellCards());
adventurePool.removeAll(cardsInUse);
} else {
cardsInUse.removeAll(AdventurePlayer.current().getNoSellCards());
adventurePool.removeAll(cardsInUse);
else if(showNoSellCards) {
adventurePool.addAll(AdventurePlayer.current().getCollectionCards(false).getFilteredPool(PaperCard::hasNoSellValue));
}
adventurePool.removeAll(cardsInUse);
if (!showNoSellCards) {
adventurePool.removeIf(PaperCard::hasNoSellValue);
}
if (showAutoSellCards) {
adventurePool.addAll(AdventurePlayer.current().getAutoSellCards());

View File

@@ -223,7 +223,6 @@ public class ConsoleCommandInterpreter {
PaperCard card = StaticData.instance().getCommonCards().getCard(s[0]);
if (card == null) return "Cannot find card: " + s[0];
Current.player().addCard(card.getNoSellVersion());
Current.player().noSellCards.add(card.getNoSellVersion());
return "Added card: " + s[0];
});
registerCommand(new String[]{"give", "item"}, s -> {

View File

@@ -356,7 +356,7 @@ public class Config {
final List<String> lines = FileUtil.readAllLines(new InputStreamReader(fileInputStream, Charset.forName(CardStorageReader.DEFAULT_CHARSET_NAME)), true);
CardRules rules = rulesReader.readCard(lines, com.google.common.io.Files.getNameWithoutExtension(cardFile.getName()));
rules.setCustom();
PaperCard card = new PaperCard(rules, CardEdition.UNKNOWN.getCode(), CardRarity.Special) {
PaperCard card = new PaperCard(rules, CardEdition.UNKNOWN_CODE, CardRarity.Special) {
@Override
public String getImageKey(boolean altState) {
return ImageKeys.ADVENTURECARD_PREFIX + getName();

View File

@@ -44,6 +44,8 @@ public class Reward {
this.card = card;
count = 0;
this.isNoSell = isNoSell;
if(isNoSell)
this.card = card.getNoSellVersion();
}
public Reward(Type type, int count) {
@@ -60,6 +62,10 @@ public class Reward {
this.deck = deck;
count = 0;
this.isNoSell = isNoSell;
if(isNoSell)
deck.getTags().add("noSell");
//Could go through the deck and replace everything in it with the noSellValue version but the tag should
//handle that later.
}
public PaperCard getCard() {

View File

@@ -1143,7 +1143,7 @@ public class CardImageRenderer {
String set = state.getSetCode();
CardRarity rarity = state.getRarity();
if (!canShow) {
set = CardEdition.UNKNOWN.getCode();
set = CardEdition.UNKNOWN_CODE;
rarity = CardRarity.Unknown;
}
if (!StringUtils.isEmpty(set)) {

View File

@@ -9,6 +9,7 @@ import forge.Forge.KeyInputAdapter;
import forge.Graphics;
import forge.assets.*;
import forge.card.CardEdition;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.deck.io.DeckPreferences;
import forge.gamemodes.limited.BoosterDraft;
@@ -1767,8 +1768,7 @@ public class FDeckEditor extends TabPageScreen<FDeckEditor> {
CardManagerPage cardSourceSection;
DeckSection destination = DeckSection.matchingSection(card);
final DeckSectionPage destinationPage = parentScreen.getPageForSection(destination);
// val for colorID setup
int val;
int markedColorCount = card.getRules().getSetColorID();
switch (deckSection) {
default:
case Main:
@@ -1811,14 +1811,18 @@ public class FDeckEditor extends TabPageScreen<FDeckEditor> {
}
}
addCommanderItems(menu, card);
if ((val = card.getRules().getSetColorID()) > 0) {
if (markedColorCount > 0) {
menu.addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblColorIdentity"), Forge.hdbuttons ? FSkinImage.HDPREFERENCE : FSkinImage.SETTINGS, e -> {
//sort options so current option is on top and selected by default
Set<String> colorChoices = new HashSet<>(MagicColor.Constant.ONLY_COLORS);
GuiChoose.getChoices(Forge.getLocalizer().getMessage("lblChooseAColor", Lang.getNumeral(val)), val, val, colorChoices, new Callback<>() {
Set<String> currentColors;
if(card.getMarkedColors() != null)
currentColors = card.getMarkedColors().stream().map(MagicColor.Color::getName).collect(Collectors.toSet());
else
currentColors = null;
String prompt = Forge.getLocalizer().getMessage("lblChooseAColor", Lang.getNumeral(markedColorCount));
GuiChoose.getChoices(prompt, markedColorCount, markedColorCount, MagicColor.Constant.ONLY_COLORS, currentColors, null, new Callback<>() {
@Override
public void run(List<String> result) {
addCard(card.getColorIDVersion(new HashSet<>(result)));
addCard(card.copyWithMarkedColors(ColorSet.fromNames(result)));
removeCard(card);
}
});
@@ -1863,14 +1867,18 @@ public class FDeckEditor extends TabPageScreen<FDeckEditor> {
}
}
addCommanderItems(menu, card);
if ((val = card.getRules().getSetColorID()) > 0) {
if (markedColorCount > 0) {
menu.addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblColorIdentity"), Forge.hdbuttons ? FSkinImage.HDPREFERENCE : FSkinImage.SETTINGS, e -> {
//sort options so current option is on top and selected by default
Set<String> colorChoices = new HashSet<>(MagicColor.Constant.ONLY_COLORS);
GuiChoose.getChoices(Forge.getLocalizer().getMessage("lblChooseAColor", Lang.getNumeral(val)), val, val, colorChoices, new Callback<>() {
Set<String> currentColors;
if(card.getMarkedColors() != null)
currentColors = card.getMarkedColors().stream().map(MagicColor.Color::getName).collect(Collectors.toSet());
else
currentColors = null;
String prompt = Forge.getLocalizer().getMessage("lblChooseAColor", Lang.getNumeral(markedColorCount));
GuiChoose.getChoices(prompt, markedColorCount, markedColorCount, MagicColor.Constant.ONLY_COLORS, currentColors, null, new Callback<>() {
@Override
public void run(List<String> result) {
addCard(card.getColorIDVersion(new HashSet<>(result)));
addCard(card.copyWithMarkedColors(ColorSet.fromNames(result)));
removeCard(card);
}
});

View File

@@ -17,6 +17,7 @@ import forge.assets.*;
import forge.assets.FSkinColor.Colors;
import forge.card.*;
import forge.card.CardRenderer.CardStackPosition;
import forge.card.mana.ManaCostShard;
import forge.deck.*;
import forge.deck.io.DeckPreferences;
import forge.game.card.CardView;
@@ -34,11 +35,8 @@ import forge.util.ImageUtil;
import forge.util.TextUtil;
import forge.util.Utils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -980,7 +978,7 @@ public class ImageView<T extends InventoryItem> extends ItemView<T> {
private boolean selected, deckSelectMode, showRanking;
private final float IMAGE_SIZE = CardRenderer.MANA_SYMBOL_SIZE;
private DeckProxy deckProxy = null;
private String colorID = null;
private String markedColors = null;
private FImageComplex deckCover = null;
private Texture dpImg = null;
//private TextureRegion tr;
@@ -1007,8 +1005,10 @@ public class ImageView<T extends InventoryItem> extends ItemView<T> {
draftRankImage = FSkinImage.DRAFTRANK_C;
}
}
if (((PaperCard) item).getColorID() != null) {
colorID = ((PaperCard) item).getColorID().stream().map(MagicColor::toSymbol).collect(Collectors.joining());
if (((PaperCard) item).getMarkedColors() != null) {
markedColors = Arrays.stream(((PaperCard) item).getMarkedColors().getOrderedShards())
.map(ManaCostShard::toString)
.collect(Collectors.joining());
}
}
}
@@ -1082,7 +1082,7 @@ public class ImageView<T extends InventoryItem> extends ItemView<T> {
cardPrice = ((ShopScene) Forge.getCurrentScene()).getCardPrice((PaperCard) item);
drawCardLabel(g, "$" + cardPrice, Color.GOLD, x, y ,w ,h);
} else {
if (((PaperCard) item).isNoSell() && itemManager.showNFSWatermark() && !Config.instance().getSettingData().disableNotForSale) {
if (((PaperCard) item).hasNoSellValue() && itemManager.showNFSWatermark() && !Config.instance().getSettingData().disableNotForSale) {
Texture nfs = Forge.getAssets().getTexture(getDefaultSkinFile("nfs.png"), false);
if (nfs != null)
g.drawImage(nfs, x, y, w, h);
@@ -1092,8 +1092,8 @@ public class ImageView<T extends InventoryItem> extends ItemView<T> {
}
}
// spire colors
if (colorID != null && !colorID.isEmpty()) {
textRenderer.drawText(g, colorID, FSkinFont.forHeight(w / 5), Color.WHITE, x, y + h / 4, w, h, y, h, false, Align.center, true);
if (markedColors != null && !markedColors.isEmpty()) {
textRenderer.drawText(g, markedColors, FSkinFont.forHeight(w / 5), Color.WHITE, x, y + h / 4, w, h, y, h, false, Align.center, true);
}
} else if (item instanceof ConquestCommander) {
CardRenderer.drawCard(g, ((ConquestCommander) item).getCard(), x, y, w, h, pos);

View File

@@ -660,7 +660,7 @@ public class MatchController extends AbstractGuiGame {
}
@Override
public <T> List<T> getChoices(final String message, final int min, final int max, final List<T> choices, final T selected, final Function<T, String> display) {
public <T> List<T> getChoices(final String message, final int min, final int max, final List<T> choices, final List<T> selected, final Function<T, String> display) {
return GuiBase.getInterface().getChoices(message, min, max, choices, selected, display);
}

View File

@@ -355,7 +355,7 @@ public class ConquestAEtherScreen extends FScreen {
caption = caption0;
options = ImmutableList.copyOf(options0);
setSelectedOption(options.get(0));
setCommand(e -> GuiChoose.getChoices(Forge.getLocalizer().getMessage("lblSelectCaptionFilter", caption), 0, 1, options, selectedOption, null, new Callback<List<AEtherFilter>>() {
setCommand(e -> GuiChoose.getChoices(Forge.getLocalizer().getMessage("lblSelectCaptionFilter", caption), 0, 1, options, Set.of(selectedOption), null, new Callback<>() {
@Override
public void run(List<AEtherFilter> result) {
if (!result.isEmpty()) {

View File

@@ -3,6 +3,7 @@ package forge.toolbox;
import static forge.card.CardRenderer.MANA_SYMBOL_SIZE;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -271,6 +272,14 @@ public class FChoiceList<T> extends FList<T> implements ActivateHandler {
setSelectedIndex(getIndexOf(choice));
}
public void setSelectedItems(Collection<T> items) {
selectedIndices.clear();
items.stream().mapToInt(this::getIndexOf).filter(i -> i >= 0).forEach(selectedIndices::add);
if(!items.isEmpty())
scrollIntoView(getIndexOf(items.iterator().next()));
onSelectionChange();
}
protected String getChoiceText(T choice) {
return choice.toString();
}

View File

@@ -230,7 +230,7 @@ public class GuiChoose {
getChoices(message, min, max, choices, null, null, callback);
}
public static <T> void getChoices(final String message, final int min, final int max, final Collection<T> choices, final T selected, final Function<T, String> display, final Callback<List<T>> callback) {
public static <T> void getChoices(final String message, final int min, final int max, final Collection<T> choices, final Collection<T> selected, final Function<T, String> display, final Callback<List<T>> callback) {
if (choices == null || choices.isEmpty()) {
if (min == 0) {
callback.run(new ArrayList<>());

View File

@@ -201,11 +201,8 @@ public class ListChooser<T> extends FContainer {
/**
* Shows the dialog and returns after the dialog was closed.
*
* @param index0 index to select when shown
* @return a boolean.
*/
public void show(final T item, final boolean selectMax) {
public void show(final Collection<T> item, final boolean selectMax) {
if (called) {
throw new IllegalStateException("Already shown");
}
@@ -226,7 +223,7 @@ public class ListChooser<T> extends FContainer {
}
}
else {
lstChoices.setSelectedItem(item);
lstChoices.setSelectedItems(item);
}
optionPane.show();
}

View File

@@ -3462,7 +3462,7 @@ AdvBossIntro34=Es gibt kein Zurück!
AdvBossIntro35=Jetzt geht es um alles oder nichts!
lblYouDied={0}, du bist gestorben!!!
lblSellFor=Verkaufen für
lblUnsellableCount=Unverkäuflich ({0})
lblUnsellable=Unverkäuflich
lblAutoSell=AUTOVERKAUF
lblNoSell=NICHT VERKAUFEN
lbltoSell=Zu verkaufen ({0}/{1})

View File

@@ -3210,7 +3210,7 @@ AdvBossIntro34=There's no turning back!
AdvBossIntro35=It's all or nothing now!
lblYouDied={0}, You Died!!!
lblSellFor=Sell for
lblUnsellableCount=Unsellable ({0})
lblUnsellable=Unsellable
lblAutoSell=AUTO-SELL
lblNoSell=NO-SELL
lbltoSell=To Sell ({0}/{1})

View File

@@ -3473,7 +3473,7 @@ AdvBossIntro34=¡No hay marcha atrás!
AdvBossIntro35=¡Ahora es todo o nada!
lblYouDied={0}, ¡¡¡Moriste!!!
lblSellFor=Vender por
lblUnsellableCount=No vendible ({0})
lblUnsellable=No vendible
lblAutoSell=AUTOVENTA
lblNoSell=NO VENDER
lbltoSell=Para vender ({0}/{1})

View File

@@ -3467,7 +3467,7 @@ AdvBossIntro34=Il n'y a pas de retour en arrière!
AdvBossIntro35=C'est tout ou rien maintenant !
lblYouDied={0}, tu es mort !!!
lblSellFor=Vendre pour
lblUnsellableCount=Invendable ({0})
lblUnsellable=Invendable
lblAutoSell=VENTE AUTOMATIQUE
lblNoSell=NON-VENTE
lbltoSell=À vendre ({0}/{1})

View File

@@ -3465,7 +3465,7 @@ AdvBossIntro34=Non si può tornare indietro!
AdvBossIntro35=Adesso è tutto o niente!
lblYouDied={0}, sei morto!!!
lblSellFor=Vendi per
lblUnsellableCount=Invendibile ({0})
lblUnsellable=Invendibile
lblAutoSell=AUTOVENDITA
lblNoSell=NON VENDITA
lbltoSell=Da vendere ({0}/{1})

View File

@@ -3461,7 +3461,7 @@ AdvBossIntro34=もう後戻りはできない!
AdvBossIntro35=もう、オール・オア・ナッシングだ!
lblYouDied={0}、死んだ!!!
lblSellFor=で売る
lblUnsellableCount=販売不可 ({0})
lblUnsellable=販売不可
lblAutoSell=自動販売
lblNoSell=販売禁止
lbltoSell=販売する ({0}/{1})

View File

@@ -3551,7 +3551,7 @@ AdvBossIntro34=Não há como voltar atrás!
AdvBossIntro35=É tudo ou nada agora!
lblYouDied={0}, você morreu!!!
lblSellFor=Vender por
lblUnsellableCount=Invendável ({0})
lblUnsellable=Invendável
lblAutoSell=VENDA AUTOMÁTICA
lblNoSell=NÃO VENDER
lbltoSell=Para Vender ({0}/{1})

View File

@@ -3452,7 +3452,7 @@ AdvBossIntro34=没有回头路了!
AdvBossIntro35=现在要么全有要么全无!
lblYouDied={0},你死了!!!
lblSellFor=售价为
lblUnsellableCount=无法出售{0}
lblUnsellable=无法出售
lblAutoSell=自动销售
lblNoSell=不卖
lbltoSell=出售({0}/{1}

View File

@@ -64,7 +64,7 @@ public enum ProtocolMethod {
showOptionDialog (Mode.SERVER, Integer.TYPE, String.class, String.class, FSkinProp.class, List/*String*/.class, Integer.TYPE),
showInputDialog (Mode.SERVER, String.class, String.class, String.class, FSkinProp.class, String.class, List/*String*/.class, Boolean.TYPE),
confirm (Mode.SERVER, Boolean.TYPE, CardView.class, String.class, Boolean.TYPE, List/*String*/.class),
getChoices (Mode.SERVER, List.class, String.class, Integer.TYPE, Integer.TYPE, List.class, Object.class, Function.class),
getChoices (Mode.SERVER, List.class, String.class, Integer.TYPE, Integer.TYPE, List.class, List.class, Function.class),
order (Mode.SERVER, List.class, String.class, String.class, Integer.TYPE, Integer.TYPE, List.class, List.class, CardView.class, Boolean.TYPE),
sideboard (Mode.SERVER, List.class, CardPool.class, CardPool.class, String.class),
chooseSingleEntityForEffect(Mode.SERVER, GameEntityView.class, String.class, List.class, DelayedReveal.class, Boolean.TYPE),

View File

@@ -246,7 +246,7 @@ public class NetGuiGame extends AbstractGuiGame {
}
@Override
public <T> List<T> getChoices(final String message, final int min, final int max, final List<T> choices, final T selected, final Function<T, String> display) {
public <T> List<T> getChoices(final String message, final int min, final int max, final List<T> choices, final List<T> selected, final Function<T, String> display) {
return sendAndWait(ProtocolMethod.getChoices, message, min, max, choices, selected, display);
}

View File

@@ -32,6 +32,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
@@ -510,11 +511,11 @@ public class QuestWinLoseController {
final List<GameFormat> formats = new ArrayList<>();
final String preferredFormat = FModel.getQuestPreferences().getPref(QPref.BOOSTER_FORMAT);
GameFormat pref = null;
Collection<GameFormat> pref = null;
for (final GameFormat f : FModel.getFormats().getSanctionedList()) {
formats.add(f);
if (f.toString().equals(preferredFormat)) {
pref = f;
pref = List.of(f);
}
}

View File

@@ -129,7 +129,7 @@ public class CardDetailUtil {
}
public static String getCurrentColors(final CardStateView c) {
return c.getColors().toEnumSet().stream().map(MagicColor.Color::getSymbol).collect(Collectors.joining());
return c.getColors().stream().map(MagicColor.Color::getSymbol).collect(Collectors.joining());
}
public static DetailColors getRarityColor(final CardRarity rarity) {
@@ -450,10 +450,10 @@ public class CardDetailUtil {
}
// chosen spire
if (card.getChosenColorID() != null && !card.getChosenColorID().isEmpty()) {
if (card.getMarkedColors() != null && !card.getMarkedColors().isColorless()) {
area.append("\n");
area.append("(").append(Localizer.getInstance().getMessage("lblSelected")).append(": ");
area.append(Lang.joinHomogenous(card.getChosenColorID().stream().map(DeckRecognizer::getLocalisedMagicColorName).collect(Collectors.toList())));
area.append(Lang.joinHomogenous(card.getMarkedColors().stream().map(MagicColor.Color::getLocalizedName).collect(Collectors.toList())));
area.append(")");
}

View File

@@ -55,7 +55,7 @@ public class GuiDownloadSetPicturesLQ extends GuiDownloadService {
}
final String setCode2 = setMapping.get(setCode3);
if (StringUtils.isBlank(setCode3) || CardEdition.UNKNOWN.getCode().equals(setCode3)) {
if (StringUtils.isBlank(setCode3) || CardEdition.UNKNOWN_CODE.equals(setCode3)) {
// we don't want cards from unknown sets
continue;
}

View File

@@ -38,7 +38,7 @@ public interface IGuiBase {
void showImageDialog(ISkinImage image, String message, String title);
int showOptionDialog(String message, String title, FSkinProp icon, List<String> options, int defaultOption);
String showInputDialog(String message, String title, FSkinProp icon, String initialInput, List<String> inputOptions, boolean isNumeric);
<T> List<T> getChoices(String message, int min, int max, Collection<T> choices, T selected, Function<T, String> display);
<T> List<T> getChoices(String message, int min, int max, Collection<T> choices, Collection<T> selected, Function<T, String> display);
<T> List<T> order(String title, String top, int remainingObjectsMin, int remainingObjectsMax, List<T> sourceChoices, List<T> destChoices);
String showFileDialog(String title, String defaultDir);
File getSaveFile(File defaultFile);

View File

@@ -150,7 +150,7 @@ public interface IGuiGame {
<T> List<T> getChoices(String message, int min, int max, List<T> choices);
<T> List<T> getChoices(String message, int min, int max, List<T> choices, T selected, Function<T, String> display);
<T> List<T> getChoices(String message, int min, int max, List<T> choices, List<T> selected, Function<T, String> display);
// Get Integer in range
Integer getInteger(String message, int min);

View File

@@ -43,7 +43,7 @@ public class SGuiChoose {
if ((choices == null) || choices.isEmpty()) {
return null;
}
final List<T> choice = SGuiChoose.getChoices(message, 0, 1, choices, selected, display);
final List<T> choice = SGuiChoose.getChoices(message, 0, 1, choices, selected == null ? null : List.of(selected), display);
return choice.isEmpty() ? null : choice.get(0);
}
@@ -147,12 +147,12 @@ public class SGuiChoose {
return getChoices(message, min, max, Arrays.asList(choices), null, null);
}
public static <T> List<T> getChoices(final String message, final int min, final int max, final T[] choices, final T selected, final Function<T, String> display) {
return getChoices(message, min, max, Arrays.asList(choices), selected, display);
return getChoices(message, min, max, Arrays.asList(choices), selected == null ? null : List.of(selected), display);
}
public static <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices) {
return getChoices(message, min, max, choices, null, null);
}
public static <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final T selected, final Function<T, String> display) {
public static <T> List<T> getChoices(final String message, final int min, final int max, final Collection<T> choices, final Collection<T> selected, final Function<T, String> display) {
return GuiBase.getInterface().getChoices(message, min, max, choices, selected, display);
}

View File

@@ -54,7 +54,7 @@ public enum ColumnDef {
NAME("lblName", "lblName", 180, false, SortState.ASC,
from -> {
if (from.getKey() instanceof PaperCard) {
String spire = ((PaperCard) from.getKey()).getColorID() == null ? "" : ((PaperCard) from.getKey()).getColorID().toString();
String spire = ((PaperCard) from.getKey()).getMarkedColors() == null ? "" : ((PaperCard) from.getKey()).getMarkedColors().toString();
String sortableName = ((PaperCard)from.getKey()).getSortableName();
return sortableName == null ? TextUtil.toSortableName(from.getKey().getName() + spire) : sortableName + spire;
}