diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java index 728ef840127..787b5742c04 100644 --- a/forge-core/src/main/java/forge/card/CardDb.java +++ b/forge-core/src/main/java/forge/card/CardDb.java @@ -37,6 +37,7 @@ import org.apache.commons.lang3.tuple.Pair; import java.util.*; import java.util.Map.Entry; +import java.util.stream.Collectors; public final class CardDb implements ICardDatabase, IDeckGenPool { public final static String foilSuffix = "+"; @@ -272,7 +273,22 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { } artIds.put(key, artIdx); - addCard(new PaperCard(cr, e.getCode(), cis.rarity, artIdx, false, cis.collectorNumber, cis.artistName)); + addCard(new PaperCard(cr, e.getCode(), cis.rarity, artIdx, false, cis.collectorNumber, cis.artistName, cis.functionalVariantName)); + } + + private boolean addFromSetByName(String cardName, CardEdition ed, CardRules cr) { + List cardsInSet = ed.getCardInSet(cardName); // empty collection if not present + if (cr.hasFunctionalVariants()) { + cardsInSet = cardsInSet.stream().filter(c -> StringUtils.isEmpty(c.functionalVariantName) + || cr.getSupportedFunctionalVariants().contains(c.functionalVariantName) + ).collect(Collectors.toList()); + } + if (cardsInSet.isEmpty()) + return false; + for (CardInSet cis : cardsInSet) { + addSetCard(ed, cis, cr); + } + return true; } public void loadCard(String cardName, String setCode, CardRules cr) { @@ -285,18 +301,10 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { if (ed == null || ed.equals(CardEdition.UNKNOWN)) { // look for all possible editions for (CardEdition e : editions) { - List cardsInSet = e.getCardInSet(cardName); // empty collection if not present - for (CardInSet cis : cardsInSet) { - addSetCard(e, cis, cr); - reIndexNecessary = true; - } + reIndexNecessary |= addFromSetByName(cardName, e, cr); } } else { - List cardsInSet = ed.getCardInSet(cardName); // empty collection if not present - for (CardInSet cis : cardsInSet) { - addSetCard(ed, cis, cr); - reIndexNecessary = true; - } + reIndexNecessary |= addFromSetByName(cardName, ed, cr); } if (reIndexNecessary) @@ -324,11 +332,20 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { for (CardEdition.CardInSet cis : e.getAllCardsInSet()) { CardRules cr = rulesByName.get(cis.name); - if (cr != null) { - addSetCard(e, cis, cr); - } else { + if (cr == null) { missingCards.add(cis.name); + continue; } + if (cr.hasFunctionalVariants()) { + if (StringUtils.isNotEmpty(cis.functionalVariantName) + && !cr.getSupportedFunctionalVariants().contains(cis.functionalVariantName)) { + //Supported card, unsupported variant. + //Could note the card as missing but since these are often un-cards, + //it's likely absent because it does something out of scope. + continue; + } + } + addSetCard(e, cis, cr); } if (isCoreExpSet && logMissingPerEdition) { if (missingCards.isEmpty()) { @@ -1229,7 +1246,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { int artIdx = IPaperCard.DEFAULT_ART_INDEX; for (CardInSet cis : e.getCardInSet(cardName)) paperCards.add(new PaperCard(rules, e.getCode(), cis.rarity, artIdx++, false, - cis.collectorNumber, cis.artistName)); + cis.collectorNumber, cis.artistName, cis.functionalVariantName)); } } else { String lastEdition = null; @@ -1249,7 +1266,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { int cardInSetIndex = Math.max(artIdx-1, 0); // make sure doesn't go below zero CardInSet cds = cardsInSet.get(cardInSetIndex); // use ArtIndex to get the right Coll. Number paperCards.add(new PaperCard(rules, lastEdition, tuple.getValue(), artIdx++, false, - cds.collectorNumber, cds.artistName)); + cds.collectorNumber, cds.artistName, cds.functionalVariantName)); } } if (paperCards.isEmpty()) { diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java index f8672c74511..afee93adf4b 100644 --- a/forge-core/src/main/java/forge/card/CardEdition.java +++ b/forge-core/src/main/java/forge/card/CardEdition.java @@ -166,12 +166,14 @@ public final class CardEdition implements Comparable { public final String collectorNumber; public final String name; public final String artistName; + public final String functionalVariantName; - public CardInSet(final String name, final String collectorNumber, final CardRarity rarity, final String artistName) { + public CardInSet(final String name, final String collectorNumber, final CardRarity rarity, final String artistName, final String functionalVariantName) { this.name = name; this.collectorNumber = collectorNumber; this.rarity = rarity; this.artistName = artistName; + this.functionalVariantName = functionalVariantName; } public String toString() { @@ -189,6 +191,10 @@ public final class CardEdition implements Comparable { sb.append(" @"); sb.append(artistName); } + if (functionalVariantName != null) { + sb.append(" $"); + sb.append(functionalVariantName); + } return sb.toString(); } @@ -567,9 +573,11 @@ public final class CardEdition implements Comparable { * cnum - grouping #2 * rarity - grouping #4 * name - grouping #5 + * artist name - grouping #7 + * functional variant name - grouping #9 */ // "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$" - "(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@]*)( @(.*))?$" + "(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@#]*)( @([^\\$]*))?( \\$(.+))?$" ); ListMultimap cardMap = ArrayListMultimap.create(); @@ -595,7 +603,8 @@ public final class CardEdition implements Comparable { CardRarity r = CardRarity.smartValueOf(matcher.group(4)); String cardName = matcher.group(5); String artistName = matcher.group(7); - CardInSet cis = new CardInSet(cardName, collectorNumber, r, artistName); + String functionalVariantName = matcher.group(9); + CardInSet cis = new CardInSet(cardName, collectorNumber, r, artistName, functionalVariantName); cardMap.put(sectionName, cis); } diff --git a/forge-core/src/main/java/forge/card/CardFace.java b/forge-core/src/main/java/forge/card/CardFace.java index d35ceb0670e..64bb5bf5bcb 100644 --- a/forge-core/src/main/java/forge/card/CardFace.java +++ b/forge-core/src/main/java/forge/card/CardFace.java @@ -1,11 +1,8 @@ package forge.card; -import java.util.ArrayList; -import java.util.Collections; -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 org.apache.commons.lang3.StringUtils; @@ -45,6 +42,7 @@ final class CardFace implements ICardFace, Cloneable { private String toughness = null; private String initialLoyalty = ""; private String defense = ""; + private Set attractionLights = null; private String nonAbilityText = null; private List keywords = null; @@ -54,6 +52,8 @@ final class CardFace implements ICardFace, Cloneable { private List replacements = null; private Map variables = null; + private Map functionalVariants = null; + // these implement ICardCharacteristics @@ -64,6 +64,7 @@ final class CardFace implements ICardFace, Cloneable { @Override public String getToughness() { return toughness; } @Override public String getInitialLoyalty() { return initialLoyalty; } @Override public String getDefense() { return defense; } + @Override public Set getAttractionLights() { return attractionLights; } @Override public String getName() { return this.name; } @Override public CardType getType() { return this.type; } @Override public ManaCost getManaCost() { return this.manaCost; } @@ -76,24 +77,35 @@ final class CardFace implements ICardFace, Cloneable { @Override public Iterable getTriggers() { return triggers; } @Override public Iterable getReplacements() { return replacements; } @Override public String getNonAbilityText() { return nonAbilityText; } - @Override public Iterable> getVariables() { return variables.entrySet(); } + @Override public Iterable> getVariables() { + if (variables == null) + return null; + return variables.entrySet(); + } @Override public String getAltName() { return this.altName; } - - public CardFace(String name0) { + + public CardFace(String name0) { this.name = name0; if ( StringUtils.isBlank(name0) ) throw new RuntimeException("Card name is empty"); } // Here come setters to allow parser supply values - void setName(String name) { this.name = name; } + void setName(String name) { this.name = name; } void setAltName(String name) { this.altName = name; } void setType(CardType type0) { this.type = type0; } void setManaCost(ManaCost manaCost0) { this.manaCost = manaCost0; } void setColor(ColorSet color0) { this.color = color0; } void setOracleText(String text) { this.oracleText = text; } - void setInitialLoyalty(String value) { this.initialLoyalty = value; } - void setDefense(String value) { this.defense = value; } + void setInitialLoyalty(String value) { this.initialLoyalty = value; } + void setDefense(String value) { this.defense = value; } + void setAttractionLights(String value) { + if (value == null) { + this.attractionLights = null; + return; + } + this.attractionLights = Arrays.stream(value.split(" ")).map(Integer::parseInt).collect(Collectors.toSet()); + } void setPtText(String value) { final String[] k = value.split("/"); @@ -129,6 +141,27 @@ final class CardFace implements ICardFace, Cloneable { void addReplacementEffect(String value) { if (null == this.replacements) { this.replacements = new ArrayList<>(); } this.replacements.add(value);} void addSVar(String key, String value) { if (null == this.variables) { this.variables = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } this.variables.put(key, value); } + + //Functional variant methods. Used for Attractions and some Un-cards, + //when cards with the same name can have different logic. + public boolean hasFunctionalVariants() { + return this.functionalVariants != null; + } + @Override public ICardFace getFunctionalVariant(String variant) { + if(this.functionalVariants == null) + return null; + return this.functionalVariants.get(variant); + } + CardFace getOrCreateFunctionalVariant(String variant) { + if (this.functionalVariants == null) { + this.functionalVariants = new HashMap<>(); + } + if (!this.functionalVariants.containsKey(variant)) { + this.functionalVariants.put(variant, new CardFace(this.name)); + } + return this.functionalVariants.get(variant); + } + void assignMissingFields() { // Most scripts do not specify color explicitly if ( null == oracleText ) { System.err.println(name + " has no Oracle text."); oracleText = ""; } @@ -142,6 +175,7 @@ final class CardFace implements ICardFace, Cloneable { if ( replacements == null ) replacements = emptyList; if ( variables == null ) variables = emptyMap; if ( null == nonAbilityText ) nonAbilityText = ""; + //Not assigning attractionLightVariants here. Too rarely used. Will test for it downstream. } diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java index 72a88f95450..236da230dc1 100644 --- a/forge-core/src/main/java/forge/card/CardRules.java +++ b/forge-core/src/main/java/forge/card/CardRules.java @@ -130,7 +130,8 @@ public final class CardRules implements ICardCharacteristics { public boolean isVariant() { CardType t = getType(); - return t.isVanguard() || t.isScheme() || t.isPlane() || t.isPhenomenon() || t.isConspiracy() || t.isDungeon(); + return t.isVanguard() || t.isScheme() || t.isPlane() || t.isPhenomenon() + || t.isConspiracy() || t.isDungeon() || t.isAttraction(); } public CardSplitType getSplitType() { @@ -246,6 +247,8 @@ public final class CardRules implements ICardCharacteristics { return mainPart.getDefense(); } + @Override public Set getAttractionLights() { return mainPart.getAttractionLights(); } + @Override public String getOracleText() { switch (splitType.getAggregationMethod()) { @@ -396,6 +399,14 @@ public final class CardRules implements ICardCharacteristics { this.deltaLife = Integer.parseInt(TextUtil.fastReplace(pt.substring(slashPos+1), "+", "")); } + private Set supportedFunctionalVariants; + public boolean hasFunctionalVariants() { + return this.supportedFunctionalVariants != null; + } + public Set getSupportedFunctionalVariants() { + return this.supportedFunctionalVariants; + } + public ColorSet getColorIdentity() { return colorIdentity; } @@ -419,6 +430,7 @@ public final class CardRules implements ICardCharacteristics { private String partnerWith = ""; private String handLife = null; private String normalizedName = ""; + private Set supportedFunctionalVariants = null; // fields to build CardAiHints private boolean removedFromAIDecks = false; @@ -453,6 +465,7 @@ public final class CardRules implements ICardCharacteristics { this.meldWith = ""; this.partnerWith = ""; this.normalizedName = ""; + this.supportedFunctionalVariants = null; } /** @@ -476,6 +489,7 @@ public final class CardRules implements ICardCharacteristics { result.partnerWith = this.partnerWith; if (StringUtils.isNotBlank(handLife)) result.setVanguardProperties(handLife); + result.supportedFunctionalVariants = this.supportedFunctionalVariants; return result; } @@ -485,7 +499,7 @@ public final class CardRules implements ICardCharacteristics { if (line.isEmpty() || line.charAt(0) == '#') { continue; } - this.parseLine(line); + this.parseLine(line, this.faces[curFace]); } this.normalizedName = filename; return this.getCard(); @@ -496,12 +510,15 @@ public final class CardRules implements ICardCharacteristics { } /** - * Parses the line. + * Parses a single line of a card script. * - * @param line - * the line + * @param line Line of text to parse. */ public final void parseLine(final String line) { + this.parseLine(line, this.faces[curFace]); + } + + private void parseLine(final String line, CardFace face) { int colonPos = line.indexOf(':'); String key = colonPos > 0 ? line.substring(0, colonPos) : line; String value = colonPos > 0 ? line.substring(1+colonPos).trim() : null; @@ -509,7 +526,7 @@ public final class CardRules implements ICardCharacteristics { switch (key.charAt(0)) { case 'A': if ("A".equals(key)) { - this.faces[curFace].addAbility(value); + face.addAbility(value); } else if ("AI".equals(key)) { colonPos = value.indexOf(':'); String variable = colonPos > 0 ? value.substring(0, colonPos) : value; @@ -525,7 +542,7 @@ public final class CardRules implements ICardCharacteristics { } else if ("ALTERNATE".equals(key)) { this.curFace = 1; } else if ("AltName".equals(key)) { - this.faces[curFace].setAltName(value); + face.setAltName(value); } break; @@ -534,7 +551,7 @@ public final class CardRules implements ICardCharacteristics { // This is forge.card.CardColor not forge.CardColor. // Why do we have two classes with the same name? ColorSet newCol = ColorSet.fromNames(value.split(",")); - this.faces[this.curFace].setColor(newCol); + face.setColor(newCol); } break; @@ -546,7 +563,7 @@ public final class CardRules implements ICardCharacteristics { } else if ("DeckHas".equals(key)) { has = new DeckHints(value); } else if ("Defense".equals(key)) { - this.faces[this.curFace].setDefense(value); + face.setDefense(value); } break; @@ -558,7 +575,7 @@ public final class CardRules implements ICardCharacteristics { case 'K': if ("K".equals(key)) { - this.faces[this.curFace].addKeyword(value); + face.addKeyword(value); if (value.startsWith("Partner:")) { this.partnerWith = value.split(":")[1]; } @@ -567,13 +584,16 @@ public final class CardRules implements ICardCharacteristics { case 'L': if ("Loyalty".equals(key)) { - this.faces[this.curFace].setInitialLoyalty(value); + face.setInitialLoyalty(value); + } + if ("Lights".equals(key)) { + face.setAttractionLights(value); } break; case 'M': if ("ManaCost".equals(key)) { - this.faces[this.curFace].setManaCost("no cost".equals(value) ? ManaCost.NO_COST + face.setManaCost("no cost".equals(value) ? ManaCost.NO_COST : new ManaCost(new ManaCostParser(value))); } else if ("MeldPair".equals(key)) { this.meldWith = value; @@ -588,25 +608,25 @@ public final class CardRules implements ICardCharacteristics { case 'O': if ("Oracle".equals(key)) { - this.faces[this.curFace].setOracleText(value); + face.setOracleText(value); } break; case 'P': if ("PT".equals(key)) { - this.faces[this.curFace].setPtText(value); + face.setPtText(value); } break; case 'R': if ("R".equals(key)) { - this.faces[this.curFace].addReplacementEffect(value); + face.addReplacementEffect(value); } break; case 'S': if ("S".equals(key)) { - this.faces[this.curFace].addStaticAbility(value); + face.addStaticAbility(value); } else if (key.startsWith("SPECIALIZE")) { if (value.equals("WHITE")) { this.curFace = 2; @@ -626,17 +646,32 @@ public final class CardRules implements ICardCharacteristics { String variable = colonPos > 0 ? value.substring(0, colonPos) : value; value = colonPos > 0 ? value.substring(1+colonPos) : null; - this.faces[curFace].addSVar(variable, value); + face.addSVar(variable, value); } break; case 'T': if ("T".equals(key)) { - this.faces[this.curFace].addTrigger(value); + face.addTrigger(value); } else if ("Types".equals(key)) { - this.faces[this.curFace].setType(CardType.parse(value, false)); + face.setType(CardType.parse(value, false)); } else if ("Text".equals(key) && !"no text".equals(value) && StringUtils.isNotBlank(value)) { - this.faces[this.curFace].setNonAbilityText(value); + face.setNonAbilityText(value); + } + break; + + case 'V': + if("Variant".equals(key)) { + if (value == null) value = ""; + colonPos = value.indexOf(':'); + if(colonPos <= 0) throw new IllegalArgumentException("Missing variant name"); + String variantName = value.substring(0, colonPos); + CardFace varFace = face.getOrCreateFunctionalVariant(variantName); + String variantLine = value.substring(1 + colonPos); + this.parseLine(variantLine, varFace); + if(this.supportedFunctionalVariants == null) + this.supportedFunctionalVariants = new HashSet<>(); + this.supportedFunctionalVariants.add(variantName); } break; } diff --git a/forge-core/src/main/java/forge/card/CardRulesPredicates.java b/forge-core/src/main/java/forge/card/CardRulesPredicates.java index 572d9dd6664..ddd5fcf1a19 100644 --- a/forge-core/src/main/java/forge/card/CardRulesPredicates.java +++ b/forge-core/src/main/java/forge/card/CardRulesPredicates.java @@ -653,6 +653,9 @@ public final class CardRulesPredicates { public static final Predicate IS_VANGUARD = CardRulesPredicates.coreType(true, CardType.CoreType.Vanguard); public static final Predicate IS_CONSPIRACY = CardRulesPredicates.coreType(true, CardType.CoreType.Conspiracy); public static final Predicate IS_DUNGEON = CardRulesPredicates.coreType(true, CardType.CoreType.Dungeon); + public static final Predicate IS_ATTRACTION = Predicates.and(Presets.IS_ARTIFACT, + CardRulesPredicates.subType("Attraction") + ); public static final Predicate IS_NON_LAND = CardRulesPredicates.coreType(false, CardType.CoreType.Land); public static final Predicate CAN_BE_BRAWL_COMMANDER = Predicates.and(Presets.IS_LEGENDARY, Predicates.or(Presets.IS_CREATURE, Presets.IS_PLANESWALKER)); diff --git a/forge-core/src/main/java/forge/card/CardType.java b/forge-core/src/main/java/forge/card/CardType.java index 786888691c5..50bb194f6d6 100644 --- a/forge-core/src/main/java/forge/card/CardType.java +++ b/forge-core/src/main/java/forge/card/CardType.java @@ -496,6 +496,9 @@ public final class CardType implements Comparable, CardTypeView { public final boolean isEquipment() { return hasSubtype("Equipment"); } @Override public final boolean isFortification() { return hasSubtype("Fortification"); } + public boolean isAttraction() { + return hasSubtype("Attraction"); + } @Override public boolean isSaga() { diff --git a/forge-core/src/main/java/forge/card/CardTypeView.java b/forge-core/src/main/java/forge/card/CardTypeView.java index 2bb6bfc0f06..b921d4ee3ab 100644 --- a/forge-core/src/main/java/forge/card/CardTypeView.java +++ b/forge-core/src/main/java/forge/card/CardTypeView.java @@ -56,6 +56,7 @@ public interface CardTypeView extends Iterable, Serializable { boolean isAura(); boolean isEquipment(); boolean isFortification(); + boolean isAttraction(); boolean isSaga(); boolean isHistoric(); diff --git a/forge-core/src/main/java/forge/card/ICardCharacteristics.java b/forge-core/src/main/java/forge/card/ICardCharacteristics.java index 974da713567..5e7bdd328bd 100644 --- a/forge-core/src/main/java/forge/card/ICardCharacteristics.java +++ b/forge-core/src/main/java/forge/card/ICardCharacteristics.java @@ -2,6 +2,8 @@ package forge.card; import forge.card.mana.ManaCost; +import java.util.Set; + public interface ICardCharacteristics { String getName(); CardType getType(); @@ -14,6 +16,7 @@ public interface ICardCharacteristics { String getToughness(); String getInitialLoyalty(); String getDefense(); + Set getAttractionLights(); String getOracleText(); } diff --git a/forge-core/src/main/java/forge/card/ICardFace.java b/forge-core/src/main/java/forge/card/ICardFace.java index d73578d6242..24fb2c86611 100644 --- a/forge-core/src/main/java/forge/card/ICardFace.java +++ b/forge-core/src/main/java/forge/card/ICardFace.java @@ -1,9 +1,12 @@ package forge.card; -/** +/** * TODO: Write javadoc for this type. * */ public interface ICardFace extends ICardCharacteristics, ICardRawAbilites, Comparable { String getAltName(); + + boolean hasFunctionalVariants(); + ICardFace getFunctionalVariant(String variant); } diff --git a/forge-core/src/main/java/forge/deck/DeckFormat.java b/forge-core/src/main/java/forge/deck/DeckFormat.java index 514af661290..60f40c61cde 100644 --- a/forge-core/src/main/java/forge/deck/DeckFormat.java +++ b/forge-core/src/main/java/forge/deck/DeckFormat.java @@ -49,7 +49,15 @@ public enum DeckFormat { // Main board: allowed size SB: restriction Max distinct non basic cards Constructed ( Range.between(60, Integer.MAX_VALUE), Range.between(0, 15), 4), QuestDeck ( Range.between(40, Integer.MAX_VALUE), Range.between(0, 15), 4), - Limited ( Range.between(40, Integer.MAX_VALUE), null, Integer.MAX_VALUE), + Limited ( Range.between(40, Integer.MAX_VALUE), null, Integer.MAX_VALUE) { + @Override + public String getAttractionDeckConformanceProblem(Deck deck) { + //Limited attraction decks have a minimum size of 3 and no singleton restriction. + if (deck.get(DeckSection.Attractions).countAll() < 3) + return "must contain at least 3 attractions, or none at all"; + return null; + } + }, Commander ( Range.is(99), Range.between(0, 10), 1, null, new Predicate() { @Override public boolean apply(PaperCard card) { @@ -321,6 +329,12 @@ public enum DeckFormat { } } + if (deck.has(DeckSection.Attractions)) { + String attractionError = getAttractionDeckConformanceProblem(deck); + if (attractionError != null) + return attractionError; + } + final int maxCopies = getMaxCardCopies(); //Must contain no more than 4 of the same card //shared among the main deck and sideboard, except @@ -365,6 +379,18 @@ public enum DeckFormat { return null; } + public String getAttractionDeckConformanceProblem(Deck deck) { + CardPool attractionDeck = deck.get(DeckSection.Attractions); + if (attractionDeck.countAll() < 10) + return "must contain at least 10 attractions, or none at all"; + for (Entry cp : attractionDeck) { + //Constructed Attraction deck must be singleton + if (attractionDeck.countByName(cp.getKey().getName(), false) > 1) + return TextUtil.concatWithSpace("contains more than 1 copy of the attraction", cp.getKey().getName()); + } + return null; + } + public static boolean canHaveAnyNumberOf(final IPaperCard icard) { return icard.getRules().getType().isBasicLand() || Iterables.contains(icard.getRules().getMainPart().getKeywords(), diff --git a/forge-core/src/main/java/forge/deck/DeckRecognizer.java b/forge-core/src/main/java/forge/deck/DeckRecognizer.java index 9b3a706ff80..68831449471 100644 --- a/forge-core/src/main/java/forge/deck/DeckRecognizer.java +++ b/forge-core/src/main/java/forge/deck/DeckRecognizer.java @@ -151,6 +151,8 @@ public class DeckRecognizer { matchedSection = DeckSection.Conspiracy; else if (sectionName.equals("planes")) matchedSection = DeckSection.Planes; + else if (sectionName.equals("attractions")) + matchedSection = DeckSection.Attractions; if (matchedSection == null) // no match found return null; diff --git a/forge-core/src/main/java/forge/deck/DeckSection.java b/forge-core/src/main/java/forge/deck/DeckSection.java index 2d5b09f9f58..d343678ce3b 100644 --- a/forge-core/src/main/java/forge/deck/DeckSection.java +++ b/forge-core/src/main/java/forge/deck/DeckSection.java @@ -13,7 +13,8 @@ public enum DeckSection { Planes(10, Validators.PLANES_VALIDATOR), Schemes(20, Validators.SCHEME_VALIDATOR), Conspiracy(0, Validators.CONSPIRACY_VALIDATOR), - Dungeon(0, Validators.DUNGEON_VALIDATOR); + Dungeon(0, Validators.DUNGEON_VALIDATOR), + Attractions(0, Validators.ATTRACTION_VALIDATOR); private final int typicalSize; // Rules enforcement is done in DeckFormat class, this is for reference only private Function fnValidator; @@ -44,6 +45,8 @@ public enum DeckSection { return Commander; if (DeckSection.Dungeon.validate(card)) return Dungeon; + if (DeckSection.Attractions.validate(card)) + return Attractions; return Main; // default } @@ -119,5 +122,10 @@ public enum DeckSection { } }; + static final Function ATTRACTION_VALIDATOR = card -> { + CardType t = card.getRules().getType(); + return t.isAttraction(); + }; + } } diff --git a/forge-core/src/main/java/forge/item/IPaperCard.java b/forge-core/src/main/java/forge/item/IPaperCard.java index b130bda035d..e4f32f2fd22 100644 --- a/forge-core/src/main/java/forge/item/IPaperCard.java +++ b/forge-core/src/main/java/forge/item/IPaperCard.java @@ -21,6 +21,7 @@ public interface IPaperCard extends InventoryItem, Serializable { int DEFAULT_ART_INDEX = 1; int NO_ART_INDEX = -1; // Placeholder when NO ArtIndex is Specified String NO_ARTIST_NAME = ""; + String NO_FUNCTIONAL_VARIANT = ""; /** * Number of filters based on CardPrinted values. @@ -243,6 +244,7 @@ public interface IPaperCard extends InventoryItem, Serializable { String getName(); String getEdition(); String getCollectorNumber(); + String getFunctionalVariant(); int getArtIndex(); boolean isFoil(); boolean isToken(); diff --git a/forge-core/src/main/java/forge/item/PaperCard.java b/forge-core/src/main/java/forge/item/PaperCard.java index 082e3efc9a8..b0448e65105 100644 --- a/forge-core/src/main/java/forge/item/PaperCard.java +++ b/forge-core/src/main/java/forge/item/PaperCard.java @@ -57,6 +57,7 @@ public class PaperCard implements Comparable, InventoryItemFromSet, private final boolean foil; private Boolean hasImage; private String sortableName; + private final String functionalVariant; // Calculated fields are below: private transient CardRarity rarity; // rarity is given in ctor when set is assigned @@ -80,6 +81,11 @@ public class PaperCard implements Comparable, InventoryItemFromSet, return collectorNumber; } + @Override + public String getFunctionalVariant() { + return functionalVariant; + } + @Override public int getArtIndex() { return artIndex; @@ -121,7 +127,7 @@ public class PaperCard implements Comparable, InventoryItemFromSet, if (this.foiledVersion == null) { this.foiledVersion = new PaperCard(this.rules, this.edition, this.rarity, - this.artIndex, true, String.valueOf(collectorNumber), this.artist); + this.artIndex, true, String.valueOf(collectorNumber), this.artist, this.functionalVariant); } return this.foiledVersion; } @@ -130,7 +136,7 @@ public class PaperCard implements Comparable, InventoryItemFromSet, return this; PaperCard unFoiledVersion = new PaperCard(this.rules, this.edition, this.rarity, - this.artIndex, false, String.valueOf(collectorNumber), this.artist); + this.artIndex, false, String.valueOf(collectorNumber), this.artist, this.functionalVariant); return unFoiledVersion; } @@ -173,11 +179,12 @@ public class PaperCard implements Comparable, InventoryItemFromSet, public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0) { this(rules0, edition0, rarity0, IPaperCard.DEFAULT_ART_INDEX, false, - IPaperCard.NO_COLLECTOR_NUMBER, IPaperCard.NO_ARTIST_NAME); + IPaperCard.NO_COLLECTOR_NUMBER, IPaperCard.NO_ARTIST_NAME, ""); } public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, - final int artIndex0, final boolean foil0, final String collectorNumber0, final String artist0) { + final int artIndex0, final boolean foil0, final String collectorNumber0, + final String artist0, final String functionalVariant) { if (rules0 == null || edition0 == null || rarity0 == null) { throw new IllegalArgumentException("Cannot create card without rules, edition or rarity"); } @@ -192,6 +199,7 @@ public class PaperCard implements Comparable, InventoryItemFromSet, // 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())); + this.functionalVariant = functionalVariant != null ? functionalVariant : IPaperCard.NO_FUNCTIONAL_VARIANT; } // Want this class to be a key for HashTable diff --git a/forge-core/src/main/java/forge/item/PaperToken.java b/forge-core/src/main/java/forge/item/PaperToken.java index b8b08e90bf7..4c3c5ef6d3d 100644 --- a/forge-core/src/main/java/forge/item/PaperToken.java +++ b/forge-core/src/main/java/forge/item/PaperToken.java @@ -150,6 +150,12 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard { return IPaperCard.NO_COLLECTOR_NUMBER; } + @Override + public String getFunctionalVariant() { + //Tokens aren't differentiated by name, so they don't really need support for this. + return IPaperCard.NO_FUNCTIONAL_VARIANT; + } + @Override public int getArtIndex() { return artIndex; diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 4c423f0f3ff..6c2fdbc0684 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -158,6 +158,16 @@ public class GameAction { } } + //717.6. If a card with an Astrotorium card back would be put into a zone other than the battlefield, exile, + //or the command zone from anywhere, instead its owner puts it into the junkyard. + if (c.isAttractionCard() && !toBattlefield && !zoneTo.is(ZoneType.AttractionDeck) + && !zoneTo.is(ZoneType.Junkyard) && !zoneTo.is(ZoneType.Exile) && !zoneTo.is(ZoneType.Command)) { + //This should technically be a replacement effect, but with the "can apply more than once to the same event" + //clause, this seems sufficient for now. + //TODO: Figure out what on earth happens if you animate an attraction, mutate a creature/commander/token onto it, and it dies... + return moveToJunkyard(c, cause, params); + } + boolean suppress = !c.isToken() && zoneFrom.equals(zoneTo); Card copied = null; @@ -449,7 +459,8 @@ public class GameAction { } game.getCombat().removeFromCombat(c); } - if ((zoneFrom.is(ZoneType.Library) || zoneFrom.is(ZoneType.PlanarDeck) || zoneFrom.is(ZoneType.SchemeDeck)) + if ((zoneFrom.is(ZoneType.Library) || zoneFrom.is(ZoneType.PlanarDeck) + || zoneFrom.is(ZoneType.SchemeDeck) || zoneFrom.is(ZoneType.AttractionDeck)) && zoneFrom == zoneTo && position.equals(zoneFrom.size()) && position != 0) { position--; } @@ -509,7 +520,7 @@ public class GameAction { if (card.isRealCommander()) { card.setMoveToCommandZone(true); } - // 723.3e & 903.9a + // 727.3e & 903.9a if (wasToken && !card.isRealToken() || card.isRealCommander()) { Map repParams = AbilityKey.mapFromAffected(card); repParams.put(AbilityKey.CardLKI, card); @@ -756,6 +767,8 @@ public class GameAction { case Stack: return moveToStack(c, cause, params); case PlanarDeck: return moveToVariantDeck(c, ZoneType.PlanarDeck, libPosition, cause, params); case SchemeDeck: return moveToVariantDeck(c, ZoneType.SchemeDeck, libPosition, cause, params); + case AttractionDeck: return moveToVariantDeck(c, ZoneType.AttractionDeck, libPosition, cause, params); + case Junkyard: return moveToJunkyard(c, cause, params); default: // sideboard will also get there return moveTo(c.getOwner().getZone(name), c, cause); } @@ -901,6 +914,11 @@ public class GameAction { } return changeZone(game.getZoneOf(c), deck, c, deckPosition, cause, params); } + + public final Card moveToJunkyard(Card c, SpellAbility cause, Map params) { + final PlayerZone junkyard = c.getOwner().getZone(ZoneType.Junkyard); + return moveTo(junkyard, c, cause, params); + } public final CardCollection exile(final CardCollection cards, SpellAbility cause, Map params) { CardCollection result = new CardCollection(); diff --git a/forge-game/src/main/java/forge/game/ability/ApiType.java b/forge-game/src/main/java/forge/game/ability/ApiType.java index a3dd09aa720..6505accc3c2 100644 --- a/forge-game/src/main/java/forge/game/ability/ApiType.java +++ b/forge-game/src/main/java/forge/game/ability/ApiType.java @@ -124,6 +124,7 @@ public enum ApiType { Mutate (MutateEffect.class), NameCard (ChooseCardNameEffect.class), NoteCounters (CountersNoteEffect.class), + OpenAttraction (OpenAttractionEffect.class), PeekAndReveal (PeekAndRevealEffect.class), PermanentCreature (PermanentCreatureEffect.class), PermanentNoncreature (PermanentNoncreatureEffect.class), diff --git a/forge-game/src/main/java/forge/game/ability/effects/OpenAttractionEffect.java b/forge-game/src/main/java/forge/game/ability/effects/OpenAttractionEffect.java new file mode 100644 index 00000000000..c41a107166c --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/OpenAttractionEffect.java @@ -0,0 +1,62 @@ +package forge.game.ability.effects; + +import forge.game.ability.AbilityKey; +import forge.game.ability.AbilityUtils; +import forge.game.ability.SpellAbilityEffect; +import forge.game.card.Card; +import forge.game.card.CardZoneTable; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.zone.PlayerZone; +import forge.game.zone.ZoneType; +import forge.util.Lang; + +import java.util.List; +import java.util.Map; + +public class OpenAttractionEffect extends SpellAbilityEffect { + @Override + protected String getStackDescription(SpellAbility sa) { + final StringBuilder sb = new StringBuilder(); + final List tgtPlayers = getDefinedPlayersOrTargeted(sa); + int amount = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa) : 1; + + if(tgtPlayers.isEmpty()) + return ""; + + sb.append(Lang.joinHomogenous(tgtPlayers)); + + if (tgtPlayers.size() > 1) { + sb.append(" each"); + } + sb.append(Lang.joinVerb(tgtPlayers, " open")).append(" "); + sb.append(amount == 1 ? "an Attraction." : (Lang.getNumeral(amount) + " Attractions.")); + return sb.toString(); + } + + @Override + public void resolve(SpellAbility sa) { + final Card source = sa.getHostCard(); + final List tgtPlayers = getDefinedPlayersOrTargeted(sa); + int amount = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa) : 1; + + Map moveParams = AbilityKey.newMap(); + final CardZoneTable triggerList = AbilityKey.addCardZoneTableParams(moveParams, sa); + + for (Player p : tgtPlayers) { + if (!p.isInGame()) + continue; + final PlayerZone attractionDeck = p.getZone(ZoneType.AttractionDeck); + for (int i = 0; i < amount; i++) { + if(attractionDeck.isEmpty()) + continue; + Card attraction = attractionDeck.get(0); + attraction = p.getGame().getAction().moveToPlay(attraction, sa, moveParams); + if (sa.hasParam("Remember")) { + source.addRemembered(attraction); + } + } + } + triggerList.triggerChangesZoneAll(sa.getHostCard().getGame(), sa); + } +} diff --git a/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java b/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java index 8e490682387..08555f6345e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java @@ -12,6 +12,7 @@ import forge.game.player.PlayerCollection; import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; import forge.game.trigger.TriggerType; +import forge.util.Lang; import forge.util.Localizer; import forge.util.MyRandom; import org.apache.commons.lang3.StringUtils; @@ -44,6 +45,13 @@ public class RollDiceEffect extends SpellAbilityEffect { protected String getStackDescription(SpellAbility sa) { final PlayerCollection player = getTargetPlayers(sa); + if(sa.hasParam("ToVisitYourAttractions")) { + if (player.size() == 1 && player.get(0).equals(sa.getActivatingPlayer())) + return "Roll to Visit Your Attractions."; + else + return String.format("%s %s to visit their Attractions.", Lang.joinHomogenous(player), Lang.joinVerb(player, "roll")); + } + StringBuilder stringBuilder = new StringBuilder(); if (player.size() == 1 && player.get(0).equals(sa.getActivatingPlayer())) { stringBuilder.append("Roll "); @@ -121,8 +129,9 @@ public class RollDiceEffect extends SpellAbilityEffect { //Notify of results if (amount > 0) { StringBuilder sb = new StringBuilder(); - sb.append(Localizer.getInstance().getMessage("lblPlayerRolledResult", player, - StringUtils.join(naturalRolls, ", "))); + String rollResults = StringUtils.join(naturalRolls, ", "); + String resultMessage = sa.hasParam("ToVisitYourAttractions") ? "lblAttractionRollResult" : "lblPlayerRolledResult"; + sb.append(Localizer.getInstance().getMessage(resultMessage, player, rollResults)); if (!ignored.isEmpty()) { sb.append("\r\n").append(Localizer.getInstance().getMessage("lblIgnoredRolls", StringUtils.join(ignored, ", "))); @@ -278,6 +287,9 @@ public class RollDiceEffect extends SpellAbilityEffect { } else { int result = rollDice(sa, player, amount, sides); results.add(result); + if (sa.hasParam("ToVisitYourAttractions")) { + player.visitAttractions(result); + } } } if (rememberHighest) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/SubgameEffect.java b/forge-game/src/main/java/forge/game/ability/effects/SubgameEffect.java index 51bcf22dba2..af848eed33e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/SubgameEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/SubgameEffect.java @@ -141,12 +141,16 @@ public class SubgameEffect extends SpellAbilityEffect { // Planes setCardsInZone(player, ZoneType.PlanarDeck, maingamePlayer.getCardsIn(ZoneType.PlanarDeck), false); + // Attractions + setCardsInZone(player, ZoneType.AttractionDeck, maingamePlayer.getCardsIn(ZoneType.AttractionDeck), false); + // Vanguard and Commanders initVariantsZonesSubgame(subgame, maingamePlayer, player); player.shuffle(null); player.getZone(ZoneType.SchemeDeck).shuffle(); player.getZone(ZoneType.PlanarDeck).shuffle(); + player.getZone(ZoneType.AttractionDeck).shuffle(); } } @@ -249,6 +253,7 @@ public class SubgameEffect extends SpellAbilityEffect { player.shuffle(sa); player.getZone(ZoneType.SchemeDeck).shuffle(); player.getZone(ZoneType.PlanarDeck).shuffle(); + player.getZone(ZoneType.AttractionDeck).shuffle(); } } diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index d3f93e712a4..16bf6aa62e9 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -262,6 +262,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private boolean isImmutable = false; private boolean isEmblem = false; private boolean isBoon = false; + private boolean isAttractionCard = false; private int exertThisTurn = 0; private PlayerCollection exertedByPlayer = new PlayerCollection(); @@ -4229,6 +4230,14 @@ public class Card extends GameEntity implements Comparable, IHasSVars { currentState.setBaseDefense(Integer.toString(n)); } + public final Set getAttractionLights() { + return currentState.getAttractionLights(); + } + public final void setAttractionLights(Set attractionLights) { + currentState.setAttractionLights(attractionLights); + } + + public final int getBasePower() { return currentState.getBasePower(); } @@ -5461,6 +5470,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public final boolean isEquipment() { return getType().isEquipment(); } public final boolean isFortification() { return getType().isFortification(); } + public final boolean isAttraction() { return getType().isAttraction(); } public final boolean isCurse() { return getType().hasSubtype("Curse"); } public final boolean isAura() { return getType().isAura(); } public final boolean isShrine() { return getType().hasSubtype("Shrine"); } @@ -5837,6 +5847,16 @@ public class Card extends GameEntity implements Comparable, IHasSVars { view.updateBoon(this); } + /** + * @return true if this is physically an Attraction card with an Astrotorium card back. False otherwise. + */ + public final boolean isAttractionCard() { + return this.isAttractionCard; + } + public final void setAttractionCard(boolean isAttractionCard) { + this.isAttractionCard = isAttractionCard; + } + /* * there are easy checkers for Color. The CardUtil functions should be made * part of the Card class, so calling out is not necessary diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index d9f9fbb7e5d..a2411251419 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -44,9 +44,7 @@ import forge.item.IPaperCard; import forge.util.CardTranslation; import forge.util.TextUtil; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; /** @@ -190,6 +188,8 @@ public class CardFactory { c.setImageKey(originalPicture); c.setToken(cp.isToken()); + c.setAttractionCard(cardRules.getType().isAttraction()); + if (c.hasAlternateState()) { if (c.isFlipCard()) { c.setState(CardStateName.Flipped, false); @@ -393,6 +393,8 @@ public class CardFactory { c.setBaseToughnessString(face.getToughness()); } + c.setAttractionLights(face.getAttractionLights()); + // SpellPermanent only for Original State if (c.getCurrentStateName() == CardStateName.Original || c.getCurrentStateName() == CardStateName.Modal || c.getCurrentStateName().toString().startsWith("Specialize")) { // this is the "default" spell for permanents like creatures and artifacts @@ -409,6 +411,73 @@ public class CardFactory { } CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities()); + + if (face.hasFunctionalVariants()) { + applyFunctionalVariant(c, face); + } + } + + private static void applyFunctionalVariant(Card c, ICardFace originalFace) { + String variantName = c.getPaperCard().getFunctionalVariant(); + if (IPaperCard.NO_FUNCTIONAL_VARIANT.equals(variantName)) + return; + ICardFace variant = originalFace.getFunctionalVariant(variantName); + if (variant == null) { + System.out.printf("Tried to apply unknown or unsupported variant - Card: \"%s\"; Variant: %s\n", originalFace.getName(), variantName); + return; + } + + if (variant.getVariables() != null) + for (Entry v : variant.getVariables()) + c.setSVar(v.getKey(), v.getValue()); + if (variant.getReplacements() != null) + for (String r : variant.getReplacements()) + c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true, c.getCurrentState())); + if (variant.getStaticAbilities() != null) + for (String s : variant.getStaticAbilities()) + c.addStaticAbility(s); + if (variant.getTriggers() != null) + for (String t : variant.getTriggers()) + c.addTrigger(TriggerHandler.parseTrigger(t, c, true, c.getCurrentState())); + + if (variant.getKeywords() != null) + c.addIntrinsicKeywords(variant.getKeywords(), false); + + if (variant.getManaCost() != ManaCost.NO_COST) + c.setManaCost(variant.getManaCost()); + if (variant.getNonAbilityText() != null) + c.setText(variant.getNonAbilityText()); + + if (!"".equals(variant.getInitialLoyalty())) + c.getCurrentState().setBaseLoyalty(variant.getInitialLoyalty()); + if (!"".equals(variant.getDefense())) + c.getCurrentState().setBaseDefense(variant.getDefense()); + + if (variant.getOracleText() != null) + c.setOracleText(variant.getOracleText()); + + if (variant.getType() != null) { + for(String type : variant.getType()) + c.addType(type); + } + + if (variant.getColor() != null) + c.setColor(variant.getColor().getColor()); + + if (variant.getIntPower() != Integer.MAX_VALUE) { + c.setBasePower(variant.getIntPower()); + c.setBasePowerString(variant.getPower()); + } + if (variant.getIntToughness() != Integer.MAX_VALUE) { + c.setBaseToughness(variant.getIntToughness()); + c.setBaseToughnessString(variant.getToughness()); + } + + if (variant.getAttractionLights() != null) + c.setAttractionLights(variant.getAttractionLights()); + + if (variant.getAbilities() != null) + CardFactoryUtil.addAbilityFactoryAbilities(c, variant.getAbilities()); } public static void copySpellAbility(SpellAbility from, SpellAbility to, final Card host, final Player p, final boolean lki, final boolean keepTextChanges) { diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 833489e8884..f10306f9af1 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -1964,6 +1964,20 @@ public class CardFactoryUtil { inst.addTrigger(parsedUpkeepTrig); inst.addTrigger(parsedSacTrigger); + } else if (keyword.startsWith("Visit")) { + final String[] k = keyword.split(":"); + //final String dbVar = card.getSVar(k[1]); + + SpellAbility sa = AbilityFactory.getAbility(card, k[1]); + String descStr = "Visit — " + sa.getDescription(); + + final String trigStr = "Mode$ VisitAttraction | TriggerZones$ Battlefield | ValidCard$ Card.Self" + + "| TriggerDescription$ " + descStr; + + final Trigger t = TriggerHandler.parseTrigger(trigStr, card, intrinsic); + t.setOverridingAbility(sa); + inst.addTrigger(t); + } else if (keyword.startsWith("Dungeon")) { final List abs = Arrays.asList(keyword.substring("Dungeon:".length()).split(",")); final Map saMap = new LinkedHashMap<>(); diff --git a/forge-game/src/main/java/forge/game/card/CardPredicates.java b/forge-game/src/main/java/forge/game/card/CardPredicates.java index 658f2f11e14..0d42fcef8cf 100644 --- a/forge-game/src/main/java/forge/game/card/CardPredicates.java +++ b/forge-game/src/main/java/forge/game/card/CardPredicates.java @@ -532,6 +532,10 @@ public final class CardPredicates { }; } + public static Predicate isAttractionWithLight(int light) { + return c -> c.isAttraction() && c.getAttractionLights().contains(light); + } + public static class Presets { /** @@ -768,6 +772,7 @@ public final class CardPredicates { return c.canBeDestroyed(); } }; + public static final Predicate ATTRACTIONS = Card::isAttraction; } public static class Accessors { diff --git a/forge-game/src/main/java/forge/game/card/CardState.java b/forge-game/src/main/java/forge/game/card/CardState.java index 0b865aad0ba..90c1303228d 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -20,6 +20,7 @@ package forge.game.card; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @@ -64,6 +65,7 @@ public class CardState extends GameObject implements IHasSVars { private String baseLoyalty = ""; private String baseDefense = ""; private KeywordCollection intrinsicKeywords = new KeywordCollection(); + private Set attractionLights = null; private final FCollection nonManaAbilities = new FCollection<>(); private final FCollection manaAbilities = new FCollection<>(); @@ -240,6 +242,15 @@ public class CardState extends GameObject implements IHasSVars { view.updateDefense(this); } + public Set getAttractionLights() { + return this.attractionLights; + } + + public final void setAttractionLights(Set attractionLights) { + this.attractionLights = attractionLights; + view.updateAttractionLights(this); + } + public final Collection getCachedKeywords() { return cachedKeywords.getValues(); } @@ -588,6 +599,7 @@ public class CardState extends GameObject implements IHasSVars { setBaseToughness(source.getBaseToughness()); setBaseLoyalty(source.getBaseLoyalty()); setBaseDefense(source.getBaseDefense()); + setAttractionLights(source.getAttractionLights()); setSVars(source.getSVars()); manaAbilities.clear(); diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index 316553768b1..21ca73a61ff 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -570,6 +570,7 @@ public class CardView extends GameEntityView { case Graveyard: case Flashback: case Stack: + case Junkyard: //cards in these zones are visible to all return true; case Exile: @@ -592,6 +593,7 @@ public class CardView extends GameEntityView { return true; case Library: case PlanarDeck: + case AttractionDeck: //cards in these zones are hidden to all unless they specify otherwise break; case SchemeDeck: @@ -795,6 +797,12 @@ public class CardView extends GameEntityView { sb.append(nonAbilityText.replaceAll("CARDNAME", getName())); } + Set attractionLights = get(TrackableProperty.AttractionLights); + if (attractionLights != null && !attractionLights.isEmpty()) { + sb.append("\r\n\r\nLights: "); + sb.append(StringUtils.join(attractionLights, ", ")); + } + sb.append(getRemembered()); Direction chosenDirection = getChosenDirection(); @@ -1010,6 +1018,8 @@ public class CardView extends GameEntityView { currentState.getView().updateKeywords(c, currentState); //update keywords even if state doesn't change currentState.getView().setOriginalColors(c); //set original Colors + currentStateView.updateAttractionLights(currentState); + CardState alternateState = isSplitCard && isFaceDown() ? c.getState(CardStateName.RightSplit) : c.getAlternateState(); if (isSplitCard && isFaceDown()) { @@ -1419,6 +1429,13 @@ public class CardView extends GameEntityView { updateDefense("0"); } + public Set getAttractionLights() { + return get(TrackableProperty.AttractionLights); + } + void updateAttractionLights(CardState c) { + set(TrackableProperty.AttractionLights, c.getAttractionLights()); + } + public String getSetCode() { return get(TrackableProperty.SetCode); } @@ -1697,6 +1714,9 @@ public class CardView extends GameEntityView { return false; return Iterables.size(getType().getCoreTypes()) > 1; } + public boolean isAttraction() { + return getType().isAttraction(); + } } //special methods for updating card and player properties as needed and returning the new collection diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index c7b09f21194..d9f523499e3 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -281,12 +281,16 @@ public class PhaseHandler implements java.io.Serializable { playerTurn.setSchemeInMotion(null); } GameEntityCounterTable table = new GameEntityCounterTable(); - // all Saga get Lore counter at the begin of pre combat + // all Sagas get a Lore counter at the beginning of pre combat for (Card c : playerTurn.getCardsIn(ZoneType.Battlefield)) { if (c.isSaga()) { c.addCounter(CounterEnumType.LORE, 1, playerTurn, table); } } + // roll for attractions if we have any + if (CardLists.count(playerTurn.getCardsIn(ZoneType.Battlefield), Presets.ATTRACTIONS) > 0) { + playerTurn.rollToVisitAttractions(); + } table.replaceCounterEffect(game, null, false); } break; diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 63ab3a02366..3d500940cd3 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -61,6 +61,7 @@ import forge.item.PaperCard; import forge.util.*; import forge.util.collect.FCollection; import forge.util.collect.FCollectionView; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; @@ -78,7 +79,8 @@ import java.util.Map.Entry; public class Player extends GameEntity implements Comparable { public static final List ALL_ZONES = Collections.unmodifiableList(Arrays.asList(ZoneType.Battlefield, ZoneType.Library, ZoneType.Graveyard, ZoneType.Hand, ZoneType.Exile, ZoneType.Command, ZoneType.Ante, - ZoneType.Sideboard, ZoneType.PlanarDeck, ZoneType.SchemeDeck, ZoneType.Merged, ZoneType.Subgame, ZoneType.None)); + ZoneType.Sideboard, ZoneType.PlanarDeck, ZoneType.SchemeDeck, ZoneType.AttractionDeck, ZoneType.Junkyard, + ZoneType.Merged, ZoneType.Subgame, ZoneType.None)); private final Map commanderDamage = Maps.newHashMap(); @@ -162,6 +164,8 @@ public class Player extends GameEntity implements Comparable { private CardCollection currentPlanes = new CardCollection(); private CardCollection planeswalkedToThisTurn = new CardCollection(); + private int attractionsVisitedThisTurn = 0; //TODO: Is "number of attractions you visited this turn" supposed to mean unique ones or just total visits? + private PlayerStatistics stats = new PlayerStatistics(); private PlayerController controller; @@ -1947,6 +1951,12 @@ public class Player extends GameEntity implements Comparable { public final List getPlaneswalkedToThisTurn() { return planeswalkedToThisTurn; } + public final void incrementAttractionsVisitedThisTurn() { + this.attractionsVisitedThisTurn++; + } + public final int getAttractionsVisitedThisTurn() { + return attractionsVisitedThisTurn; + } public final void altWinBySpellEffect(final String sourceName) { if (cantWin()) { @@ -2517,6 +2527,8 @@ public class Player extends GameEntity implements Comparable { damageReceivedThisTurn.clear(); planeswalkedToThisTurn.clear(); + attractionsVisitedThisTurn = 0; + // set last turn nr if (game.getPhaseHandler().isPlayerTurn(this)) { setBeenDealtCombatDamageSinceLastTurn(false); @@ -2952,6 +2964,14 @@ public class Player extends GameEntity implements Comparable { com.add(conspire); } + // Attractions + PlayerZone attractionDeck = getZone(ZoneType.AttractionDeck); + for (IPaperCard cp : registeredPlayer.getAttractions()) { + attractionDeck.add(Card.fromPaperCard(cp, this)); + } + if (!attractionDeck.isEmpty()) + attractionDeck.shuffle(); + // Adventure Mode items Iterable adventureItemCards = registeredPlayer.getExtraCardsInCommandZone(); if (adventureItemCards != null) { @@ -3802,4 +3822,81 @@ public class Player extends GameEntity implements Comparable { public void setCommitedCrimeThisTurn(int v) { committedCrimeThisTurn = v; } + + public void visitAttractions(int light) { + CardCollection attractions = CardLists.filter(getCardsIn(ZoneType.Battlefield), CardPredicates.isAttractionWithLight(light)); + if(attractions.isEmpty()) + return; + for (Card c : attractions) { + incrementAttractionsVisitedThisTurn(); + + final Map runParams = AbilityKey.newMap(); + runParams.put(AbilityKey.Card, c); + runParams.put(AbilityKey.Player, this); + game.getTriggerHandler().runTrigger(TriggerType.VisitAttraction, runParams, false); + } + } + public void rollToVisitAttractions() { + //Essentially a retread of RollDiceEffect.rollDiceForPlayer, but without the parts that require a spell ability. + int amount = 1, sides = 6, ignore = 0; + Map ignoreChosenMap = Maps.newHashMap(); + + final Map repParams = AbilityKey.mapFromAffected(this); + repParams.put(AbilityKey.Number, amount); + repParams.put(AbilityKey.Ignore, ignore); + repParams.put(AbilityKey.IgnoreChosen, ignoreChosenMap); + + if(getGame().getReplacementHandler().run(ReplacementType.RollDice, repParams) == ReplacementResult.Updated) { + amount = (int) repParams.get(AbilityKey.Number); + ignore = (int) repParams.get(AbilityKey.Ignore); + //noinspection unchecked + ignoreChosenMap = (Map) repParams.get(AbilityKey.IgnoreChosen); + } + if (amount == 0) + return; + int total = 0; + List naturalRolls = new ArrayList<>(); + + for (int i = 0; i < amount; i++) { + int roll = MyRandom.getRandom().nextInt(sides) + 1; + // Play the die roll sound + getGame().fireEvent(new GameEventRollDie()); + roll(); + naturalRolls.add(roll); + total += roll; + } + + naturalRolls.sort(null); + + List ignored = new ArrayList<>(); + // Ignore the lowest rolls + if (ignore > 0) { + for (int i = ignore - 1; i >= 0; --i) { + total -= naturalRolls.get(i); + ignored.add(naturalRolls.get(i)); + naturalRolls.remove(i); + } + } + // Player chooses to ignore rolls + for (Player chooser : ignoreChosenMap.keySet()) { + for (int ig = 0; ig < ignoreChosenMap.get(chooser); ig++) { + Integer ign = chooser.getController().chooseRollToIgnore(naturalRolls); + total -= ign; + ignored.add(ign); + naturalRolls.remove(ign); + } + } + + StringBuilder sb = new StringBuilder(); + String rollResults = StringUtils.join(naturalRolls, ", "); + String resultMessage = "lblAttractionRollResult"; + sb.append(Localizer.getInstance().getMessage(resultMessage, this, rollResults)); + if (!ignored.isEmpty()) { + sb.append("\r\n").append(Localizer.getInstance().getMessage("lblIgnoredRolls", + StringUtils.join(ignored, ", "))); + } + getGame().getAction().notifyOfValue(null, this, sb.toString(), null); + + this.visitAttractions(total); + } } diff --git a/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java b/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java index 6cb37ae0891..2fd56052a53 100644 --- a/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java +++ b/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java @@ -33,6 +33,7 @@ public class RegisteredPlayer { private Iterable schemes = null; private Iterable planes = null; private Iterable conspiracies = null; + private Iterable attractions = null; private List commanders = Lists.newArrayList(); private List vanguardAvatars = null; private PaperCard planeswalker = null; @@ -223,8 +224,18 @@ public class RegisteredPlayer { } } + public Iterable getAttractions() { + return attractions; + } + private void assignAttractions() { + attractions = currentDeck.has(DeckSection.Attractions) + ? currentDeck.get(DeckSection.Attractions).toFlatList() + : EmptyList; + } + public void restoreDeck() { currentDeck = (Deck) originalDeck.copyTo(originalDeck.getName()); + assignAttractions(); } public boolean useRandomFoil() { diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerType.java b/forge-game/src/main/java/forge/game/trigger/TriggerType.java index 0887fcfc3a1..b3e80b7493e 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerType.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerType.java @@ -138,6 +138,7 @@ public enum TriggerType { Unattach(TriggerUnattach.class), UntapAll(TriggerUntapAll.class), Untaps(TriggerUntaps.class), + VisitAttraction(TriggerVisitAttraction.class), Vote(TriggerVote.class); private final Constructor constructor; diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerVisitAttraction.java b/forge-game/src/main/java/forge/game/trigger/TriggerVisitAttraction.java new file mode 100644 index 00000000000..c22fad87d03 --- /dev/null +++ b/forge-game/src/main/java/forge/game/trigger/TriggerVisitAttraction.java @@ -0,0 +1,40 @@ +package forge.game.trigger; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.spellability.SpellAbility; +import forge.util.Localizer; + +import java.util.Map; + + +public class TriggerVisitAttraction extends Trigger { + + public TriggerVisitAttraction(Map params, Card host, boolean intrinsic) { + super(params, host, intrinsic); + } + + @Override + public boolean performTest(Map runParams) { + if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) { + return false; + } + if (!matchesValidParam("ValidCard", runParams.get(AbilityKey.Card))) { + return false; + } + return true; + } + + @Override + public void setTriggeringObjects(SpellAbility sa, Map runParams) { + //TODO: Attraction roll value? Person who caused the attraction roll? + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player, AbilityKey.Card); + } + + @Override + public String getImportantStackObjects(SpellAbility sa) { + //TODO: Do I even need this much? Someone would need to implement a card to visit someone else's attraction... + return Localizer.getInstance().getMessage("lblPlayer") + ": " + + sa.getTriggeringObject(AbilityKey.Player); + } +} diff --git a/forge-game/src/main/java/forge/game/zone/ZoneType.java b/forge-game/src/main/java/forge/game/zone/ZoneType.java index f8a434dd17c..05e26744e5a 100644 --- a/forge-game/src/main/java/forge/game/zone/ZoneType.java +++ b/forge-game/src/main/java/forge/game/zone/ZoneType.java @@ -25,6 +25,8 @@ public enum ZoneType { Merged(false, "lblBattlefieldZone"), SchemeDeck(true, "lblSchemeDeckZone"), PlanarDeck(true, "lblPlanarDeckZone"), + AttractionDeck(true, "lblAttractionDeckZone"), + Junkyard(false, "lblJunkyardZone"), Subgame(true, "lblSubgameZone"), None(true, "lblNoneZone"); diff --git a/forge-game/src/main/java/forge/trackable/TrackableProperty.java b/forge-game/src/main/java/forge/trackable/TrackableProperty.java index cec193e500d..3d07fae6e57 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableProperty.java +++ b/forge-game/src/main/java/forge/trackable/TrackableProperty.java @@ -125,6 +125,7 @@ public enum TrackableProperty { Toughness(TrackableTypes.IntegerType), Loyalty(TrackableTypes.StringType), Defense(TrackableTypes.StringType), + AttractionLights(TrackableTypes.IntegerSetType), ChangedColorWords(TrackableTypes.StringMapType), HasChangedColors(TrackableTypes.BooleanType), ChangedTypes(TrackableTypes.StringMapType), diff --git a/forge-game/src/main/java/forge/trackable/TrackableTypes.java b/forge-game/src/main/java/forge/trackable/TrackableTypes.java index c2d3c19b07d..4b711552ecf 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableTypes.java +++ b/forge-game/src/main/java/forge/trackable/TrackableTypes.java @@ -559,6 +559,34 @@ public class TrackableTypes { } } }; + + public static final TrackableType> IntegerSetType = new TrackableType>() { + @Override + public Set getDefaultValue() { + return null; + } + + @Override + public Set deserialize(TrackableDeserializer td, Set oldValue) { + int size = td.readInt(); + if (size > 0) { + Set set = Sets.newHashSet(); + for (int i = 0; i < size; i++) { + set.add(td.readInt()); + } + return set; + } + return null; + } + + @Override + public void serialize(TrackableSerializer ts, Set value) { + ts.write(value.size()); + for (int i : value) { + ts.write(i); + } + } + }; public static final TrackableType> IntegerMapType = new TrackableType>() { @Override public Map getDefaultValue() { diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorConstructed.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorConstructed.java index e48e46dec50..8acadf4a801 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorConstructed.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorConstructed.java @@ -56,7 +56,7 @@ public final class CEditorConstructed extends CDeckEditor { private DeckController controller; private final List allSections = new ArrayList<>(); private ItemPool normalPool, avatarPool, planePool, schemePool, conspiracyPool, - commanderPool, dungeonPool; + commanderPool, dungeonPool, attractionPool; CardManager catalogManager; CardManager deckManager; @@ -131,6 +131,9 @@ public final class CEditorConstructed extends CDeckEditor { default: } + allSections.add(DeckSection.Attractions); + attractionPool = FModel.getAttractionPool(); + catalogManager = new CardManager(getCDetailPicture(), wantUnique, false, false); deckManager = new CardManager(getCDetailPicture(), false, false, false); deckManager.setAlwaysNonUnique(true); @@ -342,6 +345,9 @@ public final class CEditorConstructed extends CDeckEditor { case Dungeon: cmb.addMoveItems(localizer.getMessage("lblAdd"), localizer.getMessage("lbltodungeondeck")); break; + case Attractions: + cmb.addMoveItems(localizer.getMessage("lblAdd"), localizer.getMessage("lbltoattractiondeck")); + break; } } @@ -374,6 +380,9 @@ public final class CEditorConstructed extends CDeckEditor { case Dungeon: cmb.addMoveItems(localizer.getMessage("lblRemove"), localizer.getMessage("lblfromdungeondeck")); break; + case Attractions: + cmb.addMoveItems(localizer.getMessage("lblRemove"), localizer.getMessage("lblfromattractiondeck")); + break; } if (foilAvailable) { cmb.addMakeFoils(); @@ -482,6 +491,12 @@ public final class CEditorConstructed extends CDeckEditor { this.getCatalogManager().setAllowMultipleSelections(true); this.getDeckManager().setPool(this.controller.getModel().getOrCreate(DeckSection.Dungeon)); break; + case Attractions: + this.getCatalogManager().setup(ItemManagerConfig.ATTRACTION_POOL); + this.getCatalogManager().setPool(attractionPool, true); + this.getCatalogManager().setAllowMultipleSelections(true); + this.getDeckManager().setPool(this.controller.getModel().getOrCreate(DeckSection.Attractions)); + break; } case Commander: case Oathbreaker: @@ -506,6 +521,12 @@ public final class CEditorConstructed extends CDeckEditor { this.getCatalogManager().setAllowMultipleSelections(false); this.getDeckManager().setPool(this.controller.getModel().getOrCreate(DeckSection.Commander)); break; + case Attractions: + this.getCatalogManager().setup(ItemManagerConfig.ATTRACTION_POOL); + this.getCatalogManager().setPool(attractionPool, true); + this.getCatalogManager().setAllowMultipleSelections(true); + this.getDeckManager().setPool(this.controller.getModel().getOrCreate(DeckSection.Attractions)); + break; default: break; } diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorLimited.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorLimited.java index f2fb69799be..18325d9c790 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorLimited.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorLimited.java @@ -113,6 +113,7 @@ public final class CEditorLimited extends CDeckEditor { allSections.add(DeckSection.Main); allSections.add(DeckSection.Conspiracy); + allSections.add(DeckSection.Attractions); this.getCbxSection().removeAllItems(); for (DeckSection section : allSections) { @@ -221,6 +222,10 @@ public final class CEditorLimited extends CDeckEditor { this.getCatalogManager().setup(ItemManagerConfig.DRAFT_CONSPIRACY); this.getDeckManager().setPool(getHumanDeck().getOrCreate(DeckSection.Conspiracy)); break; + case Attractions: + this.getCatalogManager().setup(ItemManagerConfig.ATTRACTION_POOL); + this.getDeckManager().setPool(getHumanDeck().getOrCreate(DeckSection.Attractions)); + break; case Main: this.getCatalogManager().setup(getScreen() == FScreen.DECK_EDITOR_DRAFT ? ItemManagerConfig.DRAFT_POOL : ItemManagerConfig.SEALED_POOL); this.getDeckManager().setPool(getHumanDeck().getOrCreate(DeckSection.Main)); diff --git a/forge-gui-mobile/src/forge/card/CardRenderer.java b/forge-gui-mobile/src/forge/card/CardRenderer.java index 96a537546c7..6a7317556c0 100644 --- a/forge-gui-mobile/src/forge/card/CardRenderer.java +++ b/forge-gui-mobile/src/forge/card/CardRenderer.java @@ -471,6 +471,7 @@ public class CardRenderer { public static void drawCardListItem(Graphics g, FSkinFont font, FSkinColor foreColor, FImageComplex cardArt, CardView card, String set, CardRarity rarity, int power, int toughness, String loyalty, int count, String suffix, float x, float y, float w, float h, boolean compactMode) { float cardArtHeight = h + 2 * FList.PADDING; float cardArtWidth = cardArtHeight * CARD_ART_RATIO; + CardView.CardStateView cardCurrentState = card.getCurrentState(); if (cardArt != null) { float artX = x - FList.PADDING; float artY = y - FList.PADDING; @@ -485,7 +486,7 @@ public class CardRenderer { g.drawRotatedImage(cardArt.getTexture(), artX, artY, cardArtHeight, cardArtWidth / 2, artX + cardArtWidth / 2, artY + cardArtWidth / 2, cardArt.getRegionX(), (int) srcY, (int) cardArt.getWidth(), (int) srcHeight, -90); g.drawRotatedImage(cardArt.getTexture(), artX, artY + cardArtWidth / 2, cardArtHeight, cardArtWidth / 2, artX + cardArtWidth / 2, artY + cardArtWidth / 2, cardArt.getRegionX(), (int) cardArt.getHeight() - (int) (srcY + srcHeight), (int) cardArt.getWidth(), (int) srcHeight, -90); } else if (card.getText().contains("Aftermath")) { - FImageComplex secondArt = CardRenderer.getAftermathSecondCardArt(card.getCurrentState().getImageKey()); + FImageComplex secondArt = CardRenderer.getAftermathSecondCardArt(cardCurrentState.getImageKey()); g.drawRotatedImage(cardArt.getTexture(), artX, artY, cardArtWidth, cardArtHeight / 2, artX + cardArtWidth, artY + cardArtHeight / 2, cardArt.getRegionX(), cardArt.getRegionY(), (int) cardArt.getWidth(), (int) cardArt.getHeight() / 2, 0); g.drawRotatedImage(secondArt.getTexture(), artX - cardArtHeight / 2, artY + cardArtHeight / 2, cardArtHeight / 2, cardArtWidth, artX, artY + cardArtHeight / 2, secondArt.getRegionX(), secondArt.getRegionY(), (int) secondArt.getWidth(), (int) secondArt.getHeight(), 90); } else { @@ -495,7 +496,7 @@ public class CardRenderer { //render card name and mana cost on first line float manaCostWidth = 0; - ManaCost mainManaCost = card.getCurrentState().getManaCost(); + ManaCost mainManaCost = cardCurrentState.getManaCost(); if (card.isSplitCard()) { //handle rendering both parts of split card mainManaCost = card.getLeftSplitState().getManaCost(); @@ -535,14 +536,17 @@ public class CardRenderer { drawSetLabel(g, typeFont, set, rarity, x + availableTypeWidth + SET_BOX_MARGIN, y - SET_BOX_MARGIN, setWidth, lineHeight + 2 * SET_BOX_MARGIN); } String type = CardDetailUtil.formatCardType(card.getCurrentState(), true); - if (card.getCurrentState().isCreature()) { //include P/T or Loyalty at end of type + if (cardCurrentState.isCreature()) { //include P/T or Loyalty at end of type type += " (" + power + " / " + toughness + ")"; - } else if (card.getCurrentState().isPlaneswalker()) { + } else if (cardCurrentState.isPlaneswalker()) { type += " (" + loyalty + ")"; - } else if (card.getCurrentState().getType().hasSubtype("Vehicle")) { + } else if (cardCurrentState.isVehicle()) { type += String.format(" [%s / %s]", power, toughness); - } else if (card.getCurrentState().isBattle()) { - type += " (" + card.getCurrentState().getDefense() + ")"; + } else if (cardCurrentState.isBattle()) { + type += " (" + cardCurrentState.getDefense() + ")"; + } else if (cardCurrentState.isAttraction()) { + //TODO: Probably shouldn't be non-localized text here? Not sure what to do if someone makes an attraction with no lights... + type += " (" + (cardCurrentState.getAttractionLights().isEmpty() ? "No Lights" : StringUtils.join(cardCurrentState.getAttractionLights(), ", ")) + ")"; } g.drawText(type, typeFont, foreColor, x, y, availableTypeWidth, lineHeight, false, Align.left, true); } diff --git a/forge-gui/res/cardsfolder/f/fortune_teller.txt b/forge-gui/res/cardsfolder/f/fortune_teller.txt new file mode 100644 index 00000000000..e5534b9bcf5 --- /dev/null +++ b/forge-gui/res/cardsfolder/f/fortune_teller.txt @@ -0,0 +1,12 @@ +Name:Fortune Teller +ManaCost:no cost +Types:Artifact Attraction +Variant:A:Lights:2 3 6 +Variant:B:Lights:2 4 6 +Variant:C:Lights:2 5 6 +Variant:D:Lights:3 4 6 +Variant:E:Lights:3 5 6 +Variant:F:Lights:4 5 6 +K:Visit:TrigScry +SVar:TrigScry:DB$ Scry | ScryNum$ 1 | SpellDescription$ Scry 1. +Oracle:Visit — Scry 1. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/i/information_booth.txt b/forge-gui/res/cardsfolder/i/information_booth.txt new file mode 100644 index 00000000000..4c605e1a141 --- /dev/null +++ b/forge-gui/res/cardsfolder/i/information_booth.txt @@ -0,0 +1,10 @@ +Name:Information Booth +ManaCost:no cost +Types:Artifact Attraction +Variant:A:Lights:2 6 +Variant:B:Lights:3 6 +Variant:C:Lights:4 6 +Variant:D:Lights:5 6 +K:Visit:TrigDraw +SVar:TrigDraw:DB$ Draw | SpellDescription$ Draw a card. +Oracle:Visit — Draw a card. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/p/petting_zookeeper.txt b/forge-gui/res/cardsfolder/p/petting_zookeeper.txt new file mode 100644 index 00000000000..d54dbb09f59 --- /dev/null +++ b/forge-gui/res/cardsfolder/p/petting_zookeeper.txt @@ -0,0 +1,8 @@ +Name:Petting Zookeeper +ManaCost:2 G +Types:Creature Elf Employee +PT:0/4 +K:Reach +T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigOpenAttraction | TriggerDescription$ When CARDNAME enters the battlefield, open an Attraction. +SVar:TrigOpenAttraction:DB$ OpenAttraction +Oracle:Reach\nWhen Petting Zookeeper enters the battlefield, open an Attraction. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/q/quick_fixer.txt b/forge-gui/res/cardsfolder/q/quick_fixer.txt new file mode 100644 index 00000000000..4af684fa9d1 --- /dev/null +++ b/forge-gui/res/cardsfolder/q/quick_fixer.txt @@ -0,0 +1,8 @@ +Name:Quick Fixer +ManaCost:2 B +Types:Creature Azra Employee +PT:2/3 +K:Menace +T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigOpenAttraction | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, open an Attraction. +SVar:TrigOpenAttraction:DB$ OpenAttraction +Oracle:Menace\nWhenever Quick Fixer deals combat damage to a player, open an Attraction. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/r/rad_rascal.txt b/forge-gui/res/cardsfolder/r/rad_rascal.txt new file mode 100644 index 00000000000..e7e19d231dc --- /dev/null +++ b/forge-gui/res/cardsfolder/r/rad_rascal.txt @@ -0,0 +1,7 @@ +Name:Rad Rascal +ManaCost:3 R +Types:Creature Devil Employee +PT:3/3 +T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigOpenAttraction | TriggerDescription$ When CARDNAME enters the battlefield, open an Attraction. +SVar:TrigOpenAttraction:DB$ OpenAttraction +Oracle:When Rad Rascal enters the battlefield, open an Attraction. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/r/ride_guide.txt b/forge-gui/res/cardsfolder/r/ride_guide.txt new file mode 100644 index 00000000000..557d08ab87c --- /dev/null +++ b/forge-gui/res/cardsfolder/r/ride_guide.txt @@ -0,0 +1,7 @@ +Name:Ride Guide +ManaCost:4 W +Types:Creature Human Employee +PT:4/4 +T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigOpenAttraction | TriggerDescription$ When CARDNAME enters the battlefield, open an Attraction. +SVar:TrigOpenAttraction:DB$ OpenAttraction +Oracle:When Ride Guide enters the battlefield, open an Attraction. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/s/seasoned_buttoneer.txt b/forge-gui/res/cardsfolder/s/seasoned_buttoneer.txt new file mode 100644 index 00000000000..cd12e102c62 --- /dev/null +++ b/forge-gui/res/cardsfolder/s/seasoned_buttoneer.txt @@ -0,0 +1,7 @@ +Name:Seasoned Buttoneer +ManaCost:2 U +Types:Creature Vedalken Employee +PT:2/2 +T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigOpenAttraction | TriggerDescription$ When CARDNAME enters the battlefield, open an Attraction. +SVar:TrigOpenAttraction:DB$ OpenAttraction +Oracle:When Seasoned Buttoneer enters the battlefield, open an Attraction. \ No newline at end of file diff --git a/forge-gui/res/editions/Unfinity.txt b/forge-gui/res/editions/Unfinity.txt index d744ad15908..f8902670173 100644 --- a/forge-gui/res/editions/Unfinity.txt +++ b/forge-gui/res/editions/Unfinity.txt @@ -217,13 +217,21 @@ F208 C Dart Throw @Gaboleps 209 C Drop Tower @Dmitry Burmak 210 R Ferris Wheel @Kirsten Zirngibl 211 C Foam Weapons Kiosk @Matt Gaser -212 C Fortune Teller @Jamroz Gary +212a C Fortune Teller @Jamroz Gary $A +212b C Fortune Teller @Jamroz Gary $B +212c C Fortune Teller @Jamroz Gary $C +212d C Fortune Teller @Jamroz Gary $D +212e C Fortune Teller @Jamroz Gary $E +212f C Fortune Teller @Jamroz Gary $F F213 R Gallery of Legends @Jakub Kasper F214 R Gift Shop @Matt Gaser F215 U Guess Your Fate @Bruce Brenneise 216 R Hall of Mirrors @Vincent Christiaens 217 R Haunted House @Dmitry Burmak -218 U Information Booth @Gaboleps +218a U Information Booth @Gaboleps $A +218b U Information Booth @Gaboleps $B +218c U Information Booth @Gaboleps $C +218d U Information Booth @Gaboleps $D 219 C Kiddie Coaster @Marco Bucci F220 R Log Flume @Marco Bucci F221 R Memory Test @Setor Fiadzigbey diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 1553a332cb1..94dc2f861d6 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -908,10 +908,12 @@ lblfromschemedeck=from scheme deck lblfromplanardeck=from planar deck lblfromconspiracydeck=from conspiracy deck lblfromdungeondeck=from dungeon deck +lblfromattractiondeck=from attraction deck lbltoschemedeck=to scheme deck lbltoplanardeck=to planar deck lbltoconspiracydeck=to conspiracy deck lbltodungeondeck=to dungeon deck +lbltoattractiondeck=to attraction deck lblMove=Move #VDock.java lblDock=Dock @@ -1344,6 +1346,7 @@ lblChooseOrderCardsPutIntoGraveyard=Choose order of cards to put into the gravey lblClosestToBottom=Closest to bottom lblChooseOrderCardsPutIntoPlanarDeck=Choose order of cards to put into the planar deck lblChooseOrderCardsPutIntoSchemeDeck=Choose order of cards to put into the scheme deck +lblChooseOrderCardsPutIntoAttractionDeck=Choose order of cards to put into the attraction deck lblChooseOrderCopiesCast=Choose order of copies to cast lblChooseOrderCards=Choose card order lblDelveHowManyCards=Delve how many cards? @@ -2087,6 +2090,7 @@ lblDoYouWantRevealYourHand=Do you want to reveal your hand? lblPlayerRolledResult={0} rolled {1} lblIgnoredRolls=Ignored rolls: {0} lblRerollResult=Reroll {0}? +lblAttractionRollResult={0} rolled to visit their Attractions. Result: {1}. #RollPlanarDiceEffect.java lblPlanarDiceResult=Planar dice result: {0} #SacrificeEffect.java @@ -2181,6 +2185,8 @@ lblSideboardZone=sideboard lblAnteZone=ante lblSchemeDeckZone=schemedeck lblPlanarDeckZone=planardeck +lblAttractionDeckZone=attractiondeck +lblJunkyardZone=junkyard lblSubgameZone=subgame lblNoneZone=none #BoosterDraft.java @@ -3004,6 +3010,7 @@ lblDetails=Details lblChosenColors=Chosen colors: lblLoyalty=Loyalty lblDefense=Defense +lblLights=Lights #Achievement.java lblStandard=Standard lblChaos=Chaos diff --git a/forge-gui/src/main/java/forge/deck/DeckImportController.java b/forge-gui/src/main/java/forge/deck/DeckImportController.java index f63f13760c4..511abab4469 100644 --- a/forge-gui/src/main/java/forge/deck/DeckImportController.java +++ b/forge-gui/src/main/java/forge/deck/DeckImportController.java @@ -459,8 +459,7 @@ public class DeckImportController { // Account for any [un]foiled version PaperCard cardKey; if (card.isFoil()) - cardKey = new PaperCard(card.getRules(), card.getEdition(), card.getRarity(), card.getArtIndex(), - false, card.getCollectorNumber(), card.getArtist()); + cardKey = card.getUnFoiled(); else cardKey = card.getFoiled(); diff --git a/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java b/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java index 74f5b0d9fe6..c7603507ac0 100644 --- a/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java +++ b/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.stream.Collectors; public class CardDetailUtil { @@ -210,6 +211,17 @@ public class CardDetailUtil { ptText.append(card.getDefense()); } + if (card.isAttraction()) { + ptText.append(Localizer.getInstance().getMessage("lblLights")).append(": ("); + Set lights = card.getAttractionLights(); + //TODO: It'd be really nice if the actual lights were drawn as symbols here. Need to look into that... + if (lights == null || lights.isEmpty()) + ptText.append(Localizer.getInstance().getMessage("lblNone")); + else + ptText.append(StringUtils.join(lights, ", ")); + ptText.append(")"); + } + return ptText.toString(); } diff --git a/forge-gui/src/main/java/forge/itemmanager/ItemManagerConfig.java b/forge-gui/src/main/java/forge/itemmanager/ItemManagerConfig.java index 11ff6d4a8a8..6093be9eee1 100644 --- a/forge-gui/src/main/java/forge/itemmanager/ItemManagerConfig.java +++ b/forge-gui/src/main/java/forge/itemmanager/ItemManagerConfig.java @@ -66,6 +66,8 @@ public enum ItemManagerConfig { null, null, 4, 0), PLANAR_DECK_EDITOR(SColumnUtil.getCatalogDefaultColumns(true), true, false, true, null, null, 4, 0), + ATTRACTION_POOL(SColumnUtil.getSpecialCardPoolDefaultColumns(), false, false, true, + null, null, 4, 0), COMMANDER_POOL(SColumnUtil.getCatalogDefaultColumns(true), true, false, false, null, null, 4, 0), COMMANDER_SECTION(SColumnUtil.getCatalogDefaultColumns(true), true, false, true, diff --git a/forge-gui/src/main/java/forge/model/FModel.java b/forge-gui/src/main/java/forge/model/FModel.java index 98ed3d4127e..3c9ef75582e 100644 --- a/forge-gui/src/main/java/forge/model/FModel.java +++ b/forge-gui/src/main/java/forge/model/FModel.java @@ -102,7 +102,7 @@ public final class FModel { private static GameFormat.Collection formats; private static ItemPool uniqueCardsNoAlt, allCardsNoAlt, planechaseCards, archenemyCards, brawlCommander, oathbreakerCommander, tinyLeadersCommander, commanderPool, - avatarPool, conspiracyPool, dungeonPool; + avatarPool, conspiracyPool, dungeonPool, attractionPool; public static void initialize(final IProgressBar progressBar, Function adjustPrefs) { //init version to log @@ -297,6 +297,7 @@ public final class FModel { allCardsNoAlt = getAllCardsNoAlt(); archenemyCards = getArchenemyCards(); planechaseCards = getPlanechaseCards(); + attractionPool = getAttractionPool(); if (GuiBase.getInterface().isLibgdxPort()) { //preload mobile Itempool uniqueCardsNoAlt = getUniqueCardsNoAlt(); @@ -392,6 +393,11 @@ public final class FModel { return dungeonPool; } + public static ItemPool getAttractionPool() { + if (attractionPool == null) + return ItemPool.createFrom(getMagicDb().getVariantCards().getAllCards(Predicates.compose(CardRulesPredicates.Presets.IS_ATTRACTION, PaperCard.FN_GET_RULES)), PaperCard.class); + return attractionPool; + } private static boolean keywordsLoaded = false; /** diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 3d7f5a4f0c5..cf4b65cebcf 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -1174,6 +1174,8 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont case SchemeDeck: choices = getGui().order(localizer.getMessage("lblChooseOrderCardsPutIntoSchemeDeck"), localizer.getMessage("lblClosestToTop"), choices, null); break; + case AttractionDeck: + choices = getGui().order(localizer.getMessage("lblChooseOrderCardsPutIntoAttractionDeck"), localizer.getMessage("lblClosestToTop"), choices, null); case Stack: choices = getGui().order(localizer.getMessage("lblChooseOrderCopiesCast"), localizer.getMessage("lblPutFirst"), choices, null); break; diff --git a/forge-gui/src/main/java/forge/util/DeckAIUtils.java b/forge-gui/src/main/java/forge/util/DeckAIUtils.java index 31548edd400..08b13d35890 100644 --- a/forge-gui/src/main/java/forge/util/DeckAIUtils.java +++ b/forge-gui/src/main/java/forge/util/DeckAIUtils.java @@ -33,6 +33,7 @@ public class DeckAIUtils { case Schemes: return localizer.getMessage("lblSchemeDeck"); case Conspiracy: return /* TODO localise */ "Conspiracy"; case Dungeon: return /* TODO localise */ "Dungeon"; + case Attractions: return /* TODO localize */ "Attractions"; default: return /* TODO better handling */ "UNKNOWN"; } }