From 174db0cac6e724441f7bda75093f4bca188c878f Mon Sep 17 00:00:00 2001 From: Jetz Date: Sun, 2 Jun 2024 02:08:36 -0400 Subject: [PATCH 01/20] I think I see why nobody wanted to do this --- .../src/main/java/forge/card/CardDb.java | 49 ++++++--- .../src/main/java/forge/card/CardEdition.java | 15 ++- .../src/main/java/forge/card/CardFace.java | 56 ++++++++--- .../src/main/java/forge/card/CardRules.java | 75 ++++++++++---- .../java/forge/card/CardRulesPredicates.java | 3 + .../src/main/java/forge/card/CardType.java | 3 + .../main/java/forge/card/CardTypeView.java | 1 + .../java/forge/card/ICardCharacteristics.java | 3 + .../src/main/java/forge/card/ICardFace.java | 5 +- .../src/main/java/forge/deck/DeckFormat.java | 28 +++++- .../main/java/forge/deck/DeckRecognizer.java | 2 + .../src/main/java/forge/deck/DeckSection.java | 10 +- .../src/main/java/forge/item/IPaperCard.java | 2 + .../src/main/java/forge/item/PaperCard.java | 16 ++- .../src/main/java/forge/item/PaperToken.java | 6 ++ .../src/main/java/forge/game/GameAction.java | 22 ++++- .../main/java/forge/game/ability/ApiType.java | 1 + .../ability/effects/OpenAttractionEffect.java | 62 ++++++++++++ .../game/ability/effects/RollDiceEffect.java | 16 ++- .../game/ability/effects/SubgameEffect.java | 5 + .../src/main/java/forge/game/card/Card.java | 20 ++++ .../java/forge/game/card/CardFactory.java | 75 +++++++++++++- .../java/forge/game/card/CardFactoryUtil.java | 14 +++ .../java/forge/game/card/CardPredicates.java | 5 + .../main/java/forge/game/card/CardState.java | 12 +++ .../main/java/forge/game/card/CardView.java | 20 ++++ .../java/forge/game/phase/PhaseHandler.java | 6 +- .../main/java/forge/game/player/Player.java | 99 ++++++++++++++++++- .../forge/game/player/RegisteredPlayer.java | 11 +++ .../java/forge/game/trigger/TriggerType.java | 1 + .../game/trigger/TriggerVisitAttraction.java | 40 ++++++++ .../main/java/forge/game/zone/ZoneType.java | 2 + .../forge/trackable/TrackableProperty.java | 1 + .../java/forge/trackable/TrackableTypes.java | 28 ++++++ .../controllers/CEditorConstructed.java | 23 ++++- .../controllers/CEditorLimited.java | 5 + .../src/forge/card/CardRenderer.java | 18 ++-- .../res/cardsfolder/f/fortune_teller.txt | 12 +++ .../res/cardsfolder/i/information_booth.txt | 10 ++ .../res/cardsfolder/p/petting_zookeeper.txt | 8 ++ forge-gui/res/cardsfolder/q/quick_fixer.txt | 8 ++ forge-gui/res/cardsfolder/r/rad_rascal.txt | 7 ++ forge-gui/res/cardsfolder/r/ride_guide.txt | 7 ++ .../res/cardsfolder/s/seasoned_buttoneer.txt | 7 ++ forge-gui/res/editions/Unfinity.txt | 12 ++- forge-gui/res/languages/en-US.properties | 7 ++ .../java/forge/deck/DeckImportController.java | 3 +- .../java/forge/gui/card/CardDetailUtil.java | 12 +++ .../forge/itemmanager/ItemManagerConfig.java | 2 + .../src/main/java/forge/model/FModel.java | 8 +- .../forge/player/PlayerControllerHuman.java | 2 + .../src/main/java/forge/util/DeckAIUtils.java | 1 + 52 files changed, 787 insertions(+), 79 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/ability/effects/OpenAttractionEffect.java create mode 100644 forge-game/src/main/java/forge/game/trigger/TriggerVisitAttraction.java create mode 100644 forge-gui/res/cardsfolder/f/fortune_teller.txt create mode 100644 forge-gui/res/cardsfolder/i/information_booth.txt create mode 100644 forge-gui/res/cardsfolder/p/petting_zookeeper.txt create mode 100644 forge-gui/res/cardsfolder/q/quick_fixer.txt create mode 100644 forge-gui/res/cardsfolder/r/rad_rascal.txt create mode 100644 forge-gui/res/cardsfolder/r/ride_guide.txt create mode 100644 forge-gui/res/cardsfolder/s/seasoned_buttoneer.txt 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 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 @@ private void addSetCard(CardEdition e, CardInSet cis, CardRules cr) { } 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 void loadCard(String cardName, String setCode, CardRules cr) { 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 void initialize(boolean logMissingPerEdition, boolean logMissingSummary, 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 CardRules putCard(CardRules rules, List> whenItW 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 CardRules putCard(CardRules rules, List> whenItW 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 static class CardInSet 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 String toString() { sb.append(" @"); sb.append(artistName); } + if (functionalVariantName != null) { + sb.append(" $"); + sb.append(functionalVariantName); + } return sb.toString(); } @@ -567,9 +573,11 @@ protected CardEdition read(File file) { * 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 @@ protected CardEdition read(File file) { 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 @@ public enum FaceSelectionMethod { // 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 @@ public enum FaceSelectionMethod { // private List replacements = null; private Map variables = null; + private Map functionalVariants = null; + // these implement ICardCharacteristics @@ -64,6 +64,7 @@ public enum FaceSelectionMethod { // @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 @@ public enum FaceSelectionMethod { // @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 @@ static int parsePT(String val) { 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 @@ void assignMissingFields() { // Most scripts do not specify color explicitly 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 @@ private static byte calculateColorIdentity(final ICardFace face) { 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 String getDefense() { return mainPart.getDefense(); } + @Override public Set getAttractionLights() { return mainPart.getAttractionLights(); } + @Override public String getOracleText() { switch (splitType.getAggregationMethod()) { @@ -396,6 +399,14 @@ public void setVanguardProperties(String pt) { 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 static class Reader { 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 void reset() { this.meldWith = ""; this.partnerWith = ""; this.normalizedName = ""; + this.supportedFunctionalVariants = null; } /** @@ -476,6 +489,7 @@ public final CardRules getCard() { result.partnerWith = this.partnerWith; if (StringUtils.isNotBlank(handLife)) result.setVanguardProperties(handLife); + result.supportedFunctionalVariants = this.supportedFunctionalVariants; return result; } @@ -485,7 +499,7 @@ public final CardRules readCard(final Iterable script, String filename) 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 CardRules readCard(final Iterable script) { } /** - * 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 void parseLine(final String line) { 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 void parseLine(final String line) { } 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 void parseLine(final String line) { // 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 void parseLine(final String line) { } 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 void parseLine(final String line) { 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 void parseLine(final String line) { 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 void parseLine(final String line) { 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 void parseLine(final String line) { 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 boolean apply(final CardRules subject) { 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 boolean isDungeon() { 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 @@ 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 String getDeckConformanceProblem(Deck deck) { } } + 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 String getDeckConformanceProblem(Deck deck) { 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 @@ else if (sectionName.equals("conspiracy")) 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 static DeckSection matchingSection(PaperCard card){ return Commander; if (DeckSection.Dungeon.validate(card)) return Dungeon; + if (DeckSection.Attractions.validate(card)) + return Attractions; return Main; // default } @@ -119,5 +122,10 @@ public Boolean apply(PaperCard card) { } }; + 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 boolean apply(PaperCard input) { 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 String getCollectorNumber() { return collectorNumber; } + @Override + public String getFunctionalVariant() { + return functionalVariant; + } + @Override public int getArtIndex() { return artIndex; @@ -121,7 +127,7 @@ public PaperCard getFoiled() { 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 PaperCard getUnFoiled() { 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 String apply(final PaperCard from) { 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 PaperCard(final CardRules rules0, final String edition0, final CardRarity // 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 String getCollectorNumber() { 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 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer } } + //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 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer } 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 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer 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 final Card moveTo(final ZoneType name, final Card c, final int libPositio 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 final Card moveToVariantDeck(Card c, ZoneType zone, int deckPosition, Spe } 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.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 static String makeFormatedDescription(SpellAbility sa) { 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 @@ private static int rollDiceForPlayer(SpellAbility sa, Player player, int amount, //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 void resolve(SpellAbility sa) { } 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 @@ private void prepareAllZonesSubgame(final Game maingame, final Game subgame) { // 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 void resolve(SpellAbility sa) { 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 final void setBaseDefense(final int n) { 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 final boolean isSpell() { 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 final void setBoon(final boolean isBoon0) { 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.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 static Card getCard(final IPaperCard cp, final Player owner, final int ca 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 @@ private static void readCardFace(Card c, ICardFace face) { 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 @@ private static void readCardFace(Card c, ICardFace face) { } 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 static void addTriggerAbility(final KeywordInterface inst, final Card car 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 boolean apply(final Card c) }; } + public static Predicate isAttractionWithLight(int light) { + return c -> c.isAttraction() && c.getAttractionLights().contains(light); + } + public static class Presets { /** @@ -768,6 +772,7 @@ public boolean apply(final Card c) { 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 @@ 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 final void setBaseDefense(final String string) { 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 final void copyFrom(final CardState source, final boolean lki, final Card 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 boolean canBeShownTo(final PlayerView viewer) { 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 boolean canBeShownTo(final PlayerView viewer) { 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 String getText(CardStateView state, HashMap translationsT 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 @@ void updateState(Card c) { 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 @@ void updateDefense(CardState c) { 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 boolean isNyx() { 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 @@ private final void onPhaseBegin() { 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.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 @@ 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 final void resetRingTemptedYou() { 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 void onCleanupPhase() { damageReceivedThisTurn.clear(); planeswalkedToThisTurn.clear(); + attractionsVisitedThisTurn = 0; + // set last turn nr if (game.getPhaseHandler().isPlayerTurn(this)) { setBeenDealtCombatDamageSinceLastTurn(false); @@ -2952,6 +2964,14 @@ else if (registeredPlayer.getPlaneswalker() != null) { // Planeswalker 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 int getCommittedCrimeThisTurn() { 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 void setPlaneswalker(PaperCard planeswalker0) { } } + 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 void serialize(TrackableSerializer ts, Map value) { } } }; + + 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 CEditorConstructed(final CDetailPicture cDetailPicture0, final GameType g 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 static void buildAddContextMenu(EditorContextMenuBuilder cmb, DeckSection 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 static void buildRemoveContextMenu(EditorContextMenuBuilder cmb, DeckSect 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 void setEditorMode(DeckSection sectionMode) { 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 void setEditorMode(DeckSection sectionMode) { 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 void run() { allSections.add(DeckSection.Main); allSections.add(DeckSection.Conspiracy); + allSections.add(DeckSection.Attractions); this.getCbxSection().removeAllItems(); for (DeckSection section : allSections) { @@ -221,6 +222,10 @@ public void setEditorMode(DeckSection sectionMode) { 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 static void drawCardListItem(Graphics g, FSkinFont font, FSkinColor foreC 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 static void drawCardListItem(Graphics g, FSkinFont font, FSkinColor foreC 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 static void drawCardListItem(Graphics g, FSkinFont font, FSkinColor foreC //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 static void drawCardListItem(Graphics g, FSkinFont font, FSkinColor foreC 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 PaperCard getCardFromDecklist(final PaperCard card){ // 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.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 static String formatPrimaryCharacteristic(final CardStateView card, final 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 @@ private FModel() { } //don't allow creating instance 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 void run() { allCardsNoAlt = getAllCardsNoAlt(); archenemyCards = getArchenemyCards(); planechaseCards = getPlanechaseCards(); + attractionPool = getAttractionPool(); if (GuiBase.getInterface().isLibgdxPort()) { //preload mobile Itempool uniqueCardsNoAlt = getUniqueCardsNoAlt(); @@ -392,6 +393,11 @@ public static ItemPool getDungeonPool() { 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 CardCollectionView orderMoveToZoneList(final CardCollectionView cards, fi 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 static String getLocalizedDeckSection(Localizer localizer, DeckSection d) 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"; } } From b36827292f48f7fa22ab309683e2318cea5926dd Mon Sep 17 00:00:00 2001 From: Jetz Date: Sun, 2 Jun 2024 09:28:36 -0400 Subject: [PATCH 02/20] Merge fixes, performance tweaks, updated Unfinity's Editions file --- .../src/main/java/forge/card/CardRules.java | 2 + .../java/forge/game/phase/PhaseHandler.java | 7 +- .../main/java/forge/game/player/Player.java | 3 +- forge-gui/res/editions/Unfinity.txt | 156 ++++++++++++++---- 4 files changed, 129 insertions(+), 39 deletions(-) diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java index 236da230dc1..fd1817cdec3 100644 --- a/forge-core/src/main/java/forge/card/CardRules.java +++ b/forge-core/src/main/java/forge/card/CardRules.java @@ -564,6 +564,8 @@ private void parseLine(final String line, CardFace face) { has = new DeckHints(value); } else if ("Defense".equals(key)) { face.setDefense(value); + } else if ("Draft".equals(key)) { + this.faces[this.curFace].addDraftAction(value); } break; 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 2ce27dd4801..e7405e1c4f9 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -17,10 +17,7 @@ */ package forge.game.phase; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; +import com.google.common.collect.*; import forge.game.*; import forge.game.ability.AbilityKey; import forge.game.ability.effects.AddTurnEffect; @@ -288,7 +285,7 @@ private final void onPhaseBegin() { } } // roll for attractions if we have any - if (CardLists.count(playerTurn.getCardsIn(ZoneType.Battlefield), Presets.ATTRACTIONS) > 0) { + if (Iterables.any(playerTurn.getCardsIn(ZoneType.Battlefield), Presets.ATTRACTIONS)) { playerTurn.rollToVisitAttractions(); } table.replaceCounterEffect(game, null, false); 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 ae4811cdd54..90ac2da3519 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -3840,9 +3840,8 @@ public void visitAttractions(int light) { for (Card c : attractions) { incrementAttractionsVisitedThisTurn(); - final Map runParams = AbilityKey.newMap(); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.Card, c); - runParams.put(AbilityKey.Player, this); game.getTriggerHandler().runTrigger(TriggerType.VisitAttraction, runParams, false); } } diff --git a/forge-gui/res/editions/Unfinity.txt b/forge-gui/res/editions/Unfinity.txt index f8902670173..35191714f52 100644 --- a/forge-gui/res/editions/Unfinity.txt +++ b/forge-gui/res/editions/Unfinity.txt @@ -205,49 +205,141 @@ F192 R Souvenir T-Shirt @Michael Phillippi F197 U The Big Top @Kirsten Zirngibl F198 C Nearby Planet @Bruce Brenneise F199 R Urza's Fun House @Dmitry Burmak -200 U Balloon Stand @Jakub Kasper -201 U Bounce Chamber @Dmitry Burmak -202 U Bumper Cars @Gabor Szikszai -F203 R Centrifuge @Greg Staples -204 C Clown Extruder @Marco Bucci -205 U Concession Stand @David Sladek -206 C Costume Shop @Raluca Marinescu -F207 C Cover the Spot @Jeff Miracola -F208 C Dart Throw @Gaboleps -209 C Drop Tower @Dmitry Burmak +200a U Balloon Stand @Jakub Kasper $A +200b U Balloon Stand @Jakub Kasper $B +200c U Balloon Stand @Jakub Kasper $C +200d U Balloon Stand @Jakub Kasper $D +201a U Bounce Chamber @Dmitry Burmak $A +201b U Bounce Chamber @Dmitry Burmak $B +201c U Bounce Chamber @Dmitry Burmak $C +201d U Bounce Chamber @Dmitry Burmak $D +202a U Bumper Cars @Gabor Szikszai $A +202b U Bumper Cars @Gabor Szikszai $B +202c U Bumper Cars @Gabor Szikszai $C +202d U Bumper Cars @Gabor Szikszai $D +202e U Bumper Cars @Gabor Szikszai $E +202f U Bumper Cars @Gabor Szikszai $F +F203a R Centrifuge @Greg Staples $A +F203b R Centrifuge @Greg Staples $B +204a C Clown Extruder @Marco Bucci $A +204b C Clown Extruder @Marco Bucci $B +204c C Clown Extruder @Marco Bucci $C +204d C Clown Extruder @Marco Bucci $D +205a U Concession Stand @David Sladek $A +205b U Concession Stand @David Sladek $B +205c U Concession Stand @David Sladek $C +205d U Concession Stand @David Sladek $D +206a C Costume Shop @Raluca Marinescu $A +206b C Costume Shop @Raluca Marinescu $B +206c C Costume Shop @Raluca Marinescu $C +206d C Costume Shop @Raluca Marinescu $D +206e C Costume Shop @Raluca Marinescu $E +206f C Costume Shop @Raluca Marinescu $F +F207a C Cover the Spot @Jeff Miracola $A +F207b C Cover the Spot @Jeff Miracola $B +F207c C Cover the Spot @Jeff Miracola $C +F207d C Cover the Spot @Jeff Miracola $D +F208a C Dart Throw @Gaboleps $A +F208b C Dart Throw @Gaboleps $B +F208c C Dart Throw @Gaboleps $C +F208d C Dart Throw @Gaboleps $D +209a C Drop Tower @Dmitry Burmak $A +209b C Drop Tower @Dmitry Burmak $B +209c C Drop Tower @Dmitry Burmak $C +209d C Drop Tower @Dmitry Burmak $D +209e C Drop Tower @Dmitry Burmak $E +209f C Drop Tower @Dmitry Burmak $F 210 R Ferris Wheel @Kirsten Zirngibl -211 C Foam Weapons Kiosk @Matt Gaser +211a C Foam Weapons Kiosk @Matt Gaser $A +211b C Foam Weapons Kiosk @Matt Gaser $B +211c C Foam Weapons Kiosk @Matt Gaser $C +211d C Foam Weapons Kiosk @Matt Gaser $D 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 +F213a R Gallery of Legends @Jakub Kasper $A +F213b R Gallery of Legends @Jakub Kasper $B +F214a R Gift Shop @Matt Gaser $A +F214b R Gift Shop @Matt Gaser $B +F215a U Guess Your Fate @Bruce Brenneise $A +F215b U Guess Your Fate @Bruce Brenneise $B +F215c U Guess Your Fate @Bruce Brenneise $C +F215d U Guess Your Fate @Bruce Brenneise $D +216a R Hall of Mirrors @Vincent Christiaens $A +216b R Hall of Mirrors @Vincent Christiaens $B +217a R Haunted House @Dmitry Burmak $A +217b R Haunted House @Dmitry Burmak $B 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 -222 R Merry-Go-Round @Carl Critchlow -223 C Pick-a-Beeble @Dave Greco -F224 R Push Your Luck @Sebastian Giacobino -225 U Roller Coaster @Gabor Szikszai -F226 U Scavenger Hunt @Jamroz Gary -227 C Spinny Ride @Aaron J. Riley -F228 U Squirrel Stack @Andrea Radeck -229 R Storybook Ride @Dmitry Burmak -F230 U The Superlatorium @Simon Dominic -231 R Swinging Ship @Mike Burns -232 U Trash Bin @Greg Bobrowski -F233 U Trivia Contest @Caroline Gariba -234 R Tunnel of Love @Vladimir Krisetskiy +219a C Kiddie Coaster @Marco Bucci $A +219b C Kiddie Coaster @Marco Bucci $B +219c C Kiddie Coaster @Marco Bucci $C +219d C Kiddie Coaster @Marco Bucci $D +219e C Kiddie Coaster @Marco Bucci $E +219f C Kiddie Coaster @Marco Bucci $F +F220a R Log Flume @Marco Bucci $A +F220b R Log Flume @Marco Bucci $B +F221a R Memory Test @Setor Fiadzigbey $A +F221b R Memory Test @Setor Fiadzigbey $B +222a R Merry-Go-Round @Carl Critchlow $A +222b R Merry-Go-Round @Carl Critchlow $B +223a C Pick-a-Beeble @Dave Greco $A +223b C Pick-a-Beeble @Dave Greco $B +223c C Pick-a-Beeble @Dave Greco $C +223d C Pick-a-Beeble @Dave Greco $D +223e C Pick-a-Beeble @Dave Greco $E +223f C Pick-a-Beeble @Dave Greco $F +F224a R Push Your Luck @Sebastian Giacobino $A +F224b R Push Your Luck @Sebastian Giacobino $B +225a U Roller Coaster @Gabor Szikszai $A +225b U Roller Coaster @Gabor Szikszai $B +225c U Roller Coaster @Gabor Szikszai $C +225d U Roller Coaster @Gabor Szikszai $D +F226a U Scavenger Hunt @Jamroz Gary $A +F226b U Scavenger Hunt @Jamroz Gary $B +F226c U Scavenger Hunt @Jamroz Gary $C +F226d U Scavenger Hunt @Jamroz Gary $D +F226e U Scavenger Hunt @Jamroz Gary $E +F226f U Scavenger Hunt @Jamroz Gary $F +227a C Spinny Ride @Aaron J. Riley $A +227b C Spinny Ride @Aaron J. Riley $B +227c C Spinny Ride @Aaron J. Riley $C +227d C Spinny Ride @Aaron J. Riley $D +227e C Spinny Ride @Aaron J. Riley $E +227f C Spinny Ride @Aaron J. Riley $F +F228a U Squirrel Stack @Andrea Radeck $A +F228b U Squirrel Stack @Andrea Radeck $B +F228c U Squirrel Stack @Andrea Radeck $C +F228d U Squirrel Stack @Andrea Radeck $D +F228e U Squirrel Stack @Andrea Radeck $E +F228f U Squirrel Stack @Andrea Radeck $F +229a R Storybook Ride @Dmitry Burmak $A +229b R Storybook Ride @Dmitry Burmak $B +F230a U The Superlatorium @Simon Dominic $A +F230b U The Superlatorium @Simon Dominic $B +F230c U The Superlatorium @Simon Dominic $C +F230d U The Superlatorium @Simon Dominic $D +F230e U The Superlatorium @Simon Dominic $E +F230f U The Superlatorium @Simon Dominic $F +231a R Swinging Ship @Mike Burns $A +231b R Swinging Ship @Mike Burns $B +232a U Trash Bin @Greg Bobrowski $A +232b U Trash Bin @Greg Bobrowski $B +232c U Trash Bin @Greg Bobrowski $C +232d U Trash Bin @Greg Bobrowski $D +F233a U Trivia Contest @Caroline Gariba $A +F233b U Trivia Contest @Caroline Gariba $B +F233c U Trivia Contest @Caroline Gariba $C +F233d U Trivia Contest @Caroline Gariba $D +F233e U Trivia Contest @Caroline Gariba $E +F233f U Trivia Contest @Caroline Gariba $F +234a R Tunnel of Love @Vladimir Krisetskiy $A +234b R Tunnel of Love @Vladimir Krisetskiy $B 235 L Plains @Adam Paquette 236 L Island @Adam Paquette 237 L Swamp @Adam Paquette From beed2ad669b2c65e3f0ad332373a06afeabd0d2a Mon Sep 17 00:00:00 2001 From: Jetz Date: Sun, 2 Jun 2024 09:28:36 -0400 Subject: [PATCH 03/20] Merge fixes, performance tweaks, updated Unfinity's Editions file --- .../src/main/java/forge/card/CardRules.java | 2 + .../java/forge/game/phase/PhaseHandler.java | 7 +- .../main/java/forge/game/player/Player.java | 3 +- forge-gui/res/editions/Unfinity.txt | 156 ++++++++++++++---- 4 files changed, 129 insertions(+), 39 deletions(-) diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java index 236da230dc1..2cd05fa6642 100644 --- a/forge-core/src/main/java/forge/card/CardRules.java +++ b/forge-core/src/main/java/forge/card/CardRules.java @@ -564,6 +564,8 @@ private void parseLine(final String line, CardFace face) { has = new DeckHints(value); } else if ("Defense".equals(key)) { face.setDefense(value); + } else if ("Draft".equals(key)) { + face.addDraftAction(value); } break; 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 2ce27dd4801..e7405e1c4f9 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -17,10 +17,7 @@ */ package forge.game.phase; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; +import com.google.common.collect.*; import forge.game.*; import forge.game.ability.AbilityKey; import forge.game.ability.effects.AddTurnEffect; @@ -288,7 +285,7 @@ private final void onPhaseBegin() { } } // roll for attractions if we have any - if (CardLists.count(playerTurn.getCardsIn(ZoneType.Battlefield), Presets.ATTRACTIONS) > 0) { + if (Iterables.any(playerTurn.getCardsIn(ZoneType.Battlefield), Presets.ATTRACTIONS)) { playerTurn.rollToVisitAttractions(); } table.replaceCounterEffect(game, null, false); 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 ae4811cdd54..90ac2da3519 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -3840,9 +3840,8 @@ public void visitAttractions(int light) { for (Card c : attractions) { incrementAttractionsVisitedThisTurn(); - final Map runParams = AbilityKey.newMap(); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.Card, c); - runParams.put(AbilityKey.Player, this); game.getTriggerHandler().runTrigger(TriggerType.VisitAttraction, runParams, false); } } diff --git a/forge-gui/res/editions/Unfinity.txt b/forge-gui/res/editions/Unfinity.txt index f8902670173..35191714f52 100644 --- a/forge-gui/res/editions/Unfinity.txt +++ b/forge-gui/res/editions/Unfinity.txt @@ -205,49 +205,141 @@ F192 R Souvenir T-Shirt @Michael Phillippi F197 U The Big Top @Kirsten Zirngibl F198 C Nearby Planet @Bruce Brenneise F199 R Urza's Fun House @Dmitry Burmak -200 U Balloon Stand @Jakub Kasper -201 U Bounce Chamber @Dmitry Burmak -202 U Bumper Cars @Gabor Szikszai -F203 R Centrifuge @Greg Staples -204 C Clown Extruder @Marco Bucci -205 U Concession Stand @David Sladek -206 C Costume Shop @Raluca Marinescu -F207 C Cover the Spot @Jeff Miracola -F208 C Dart Throw @Gaboleps -209 C Drop Tower @Dmitry Burmak +200a U Balloon Stand @Jakub Kasper $A +200b U Balloon Stand @Jakub Kasper $B +200c U Balloon Stand @Jakub Kasper $C +200d U Balloon Stand @Jakub Kasper $D +201a U Bounce Chamber @Dmitry Burmak $A +201b U Bounce Chamber @Dmitry Burmak $B +201c U Bounce Chamber @Dmitry Burmak $C +201d U Bounce Chamber @Dmitry Burmak $D +202a U Bumper Cars @Gabor Szikszai $A +202b U Bumper Cars @Gabor Szikszai $B +202c U Bumper Cars @Gabor Szikszai $C +202d U Bumper Cars @Gabor Szikszai $D +202e U Bumper Cars @Gabor Szikszai $E +202f U Bumper Cars @Gabor Szikszai $F +F203a R Centrifuge @Greg Staples $A +F203b R Centrifuge @Greg Staples $B +204a C Clown Extruder @Marco Bucci $A +204b C Clown Extruder @Marco Bucci $B +204c C Clown Extruder @Marco Bucci $C +204d C Clown Extruder @Marco Bucci $D +205a U Concession Stand @David Sladek $A +205b U Concession Stand @David Sladek $B +205c U Concession Stand @David Sladek $C +205d U Concession Stand @David Sladek $D +206a C Costume Shop @Raluca Marinescu $A +206b C Costume Shop @Raluca Marinescu $B +206c C Costume Shop @Raluca Marinescu $C +206d C Costume Shop @Raluca Marinescu $D +206e C Costume Shop @Raluca Marinescu $E +206f C Costume Shop @Raluca Marinescu $F +F207a C Cover the Spot @Jeff Miracola $A +F207b C Cover the Spot @Jeff Miracola $B +F207c C Cover the Spot @Jeff Miracola $C +F207d C Cover the Spot @Jeff Miracola $D +F208a C Dart Throw @Gaboleps $A +F208b C Dart Throw @Gaboleps $B +F208c C Dart Throw @Gaboleps $C +F208d C Dart Throw @Gaboleps $D +209a C Drop Tower @Dmitry Burmak $A +209b C Drop Tower @Dmitry Burmak $B +209c C Drop Tower @Dmitry Burmak $C +209d C Drop Tower @Dmitry Burmak $D +209e C Drop Tower @Dmitry Burmak $E +209f C Drop Tower @Dmitry Burmak $F 210 R Ferris Wheel @Kirsten Zirngibl -211 C Foam Weapons Kiosk @Matt Gaser +211a C Foam Weapons Kiosk @Matt Gaser $A +211b C Foam Weapons Kiosk @Matt Gaser $B +211c C Foam Weapons Kiosk @Matt Gaser $C +211d C Foam Weapons Kiosk @Matt Gaser $D 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 +F213a R Gallery of Legends @Jakub Kasper $A +F213b R Gallery of Legends @Jakub Kasper $B +F214a R Gift Shop @Matt Gaser $A +F214b R Gift Shop @Matt Gaser $B +F215a U Guess Your Fate @Bruce Brenneise $A +F215b U Guess Your Fate @Bruce Brenneise $B +F215c U Guess Your Fate @Bruce Brenneise $C +F215d U Guess Your Fate @Bruce Brenneise $D +216a R Hall of Mirrors @Vincent Christiaens $A +216b R Hall of Mirrors @Vincent Christiaens $B +217a R Haunted House @Dmitry Burmak $A +217b R Haunted House @Dmitry Burmak $B 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 -222 R Merry-Go-Round @Carl Critchlow -223 C Pick-a-Beeble @Dave Greco -F224 R Push Your Luck @Sebastian Giacobino -225 U Roller Coaster @Gabor Szikszai -F226 U Scavenger Hunt @Jamroz Gary -227 C Spinny Ride @Aaron J. Riley -F228 U Squirrel Stack @Andrea Radeck -229 R Storybook Ride @Dmitry Burmak -F230 U The Superlatorium @Simon Dominic -231 R Swinging Ship @Mike Burns -232 U Trash Bin @Greg Bobrowski -F233 U Trivia Contest @Caroline Gariba -234 R Tunnel of Love @Vladimir Krisetskiy +219a C Kiddie Coaster @Marco Bucci $A +219b C Kiddie Coaster @Marco Bucci $B +219c C Kiddie Coaster @Marco Bucci $C +219d C Kiddie Coaster @Marco Bucci $D +219e C Kiddie Coaster @Marco Bucci $E +219f C Kiddie Coaster @Marco Bucci $F +F220a R Log Flume @Marco Bucci $A +F220b R Log Flume @Marco Bucci $B +F221a R Memory Test @Setor Fiadzigbey $A +F221b R Memory Test @Setor Fiadzigbey $B +222a R Merry-Go-Round @Carl Critchlow $A +222b R Merry-Go-Round @Carl Critchlow $B +223a C Pick-a-Beeble @Dave Greco $A +223b C Pick-a-Beeble @Dave Greco $B +223c C Pick-a-Beeble @Dave Greco $C +223d C Pick-a-Beeble @Dave Greco $D +223e C Pick-a-Beeble @Dave Greco $E +223f C Pick-a-Beeble @Dave Greco $F +F224a R Push Your Luck @Sebastian Giacobino $A +F224b R Push Your Luck @Sebastian Giacobino $B +225a U Roller Coaster @Gabor Szikszai $A +225b U Roller Coaster @Gabor Szikszai $B +225c U Roller Coaster @Gabor Szikszai $C +225d U Roller Coaster @Gabor Szikszai $D +F226a U Scavenger Hunt @Jamroz Gary $A +F226b U Scavenger Hunt @Jamroz Gary $B +F226c U Scavenger Hunt @Jamroz Gary $C +F226d U Scavenger Hunt @Jamroz Gary $D +F226e U Scavenger Hunt @Jamroz Gary $E +F226f U Scavenger Hunt @Jamroz Gary $F +227a C Spinny Ride @Aaron J. Riley $A +227b C Spinny Ride @Aaron J. Riley $B +227c C Spinny Ride @Aaron J. Riley $C +227d C Spinny Ride @Aaron J. Riley $D +227e C Spinny Ride @Aaron J. Riley $E +227f C Spinny Ride @Aaron J. Riley $F +F228a U Squirrel Stack @Andrea Radeck $A +F228b U Squirrel Stack @Andrea Radeck $B +F228c U Squirrel Stack @Andrea Radeck $C +F228d U Squirrel Stack @Andrea Radeck $D +F228e U Squirrel Stack @Andrea Radeck $E +F228f U Squirrel Stack @Andrea Radeck $F +229a R Storybook Ride @Dmitry Burmak $A +229b R Storybook Ride @Dmitry Burmak $B +F230a U The Superlatorium @Simon Dominic $A +F230b U The Superlatorium @Simon Dominic $B +F230c U The Superlatorium @Simon Dominic $C +F230d U The Superlatorium @Simon Dominic $D +F230e U The Superlatorium @Simon Dominic $E +F230f U The Superlatorium @Simon Dominic $F +231a R Swinging Ship @Mike Burns $A +231b R Swinging Ship @Mike Burns $B +232a U Trash Bin @Greg Bobrowski $A +232b U Trash Bin @Greg Bobrowski $B +232c U Trash Bin @Greg Bobrowski $C +232d U Trash Bin @Greg Bobrowski $D +F233a U Trivia Contest @Caroline Gariba $A +F233b U Trivia Contest @Caroline Gariba $B +F233c U Trivia Contest @Caroline Gariba $C +F233d U Trivia Contest @Caroline Gariba $D +F233e U Trivia Contest @Caroline Gariba $E +F233f U Trivia Contest @Caroline Gariba $F +234a R Tunnel of Love @Vladimir Krisetskiy $A +234b R Tunnel of Love @Vladimir Krisetskiy $B 235 L Plains @Adam Paquette 236 L Island @Adam Paquette 237 L Swamp @Adam Paquette From 8c8c8a779bb6b765b42b2ddcebd7c88d00d6f9f9 Mon Sep 17 00:00:00 2001 From: Jetz Date: Fri, 7 Jun 2024 21:36:10 -0400 Subject: [PATCH 04/20] AttractionsYouVisitedThisTurn and Squirrel Squatters Untangled some oracle text methods in Card and CardState Sly Spy, Everythingamajig, and two Garbage Elementals Visit card text fix. 2 more Attractions --- .../src/main/java/forge/card/CardEdition.java | 2 +- .../java/forge/game/ability/AbilityUtils.java | 4 ++ .../game/ability/effects/RollDiceEffect.java | 9 ++- .../src/main/java/forge/game/card/Card.java | 18 ++--- .../java/forge/game/card/CardFactory.java | 10 +-- .../main/java/forge/game/card/CardState.java | 11 +++ .../main/java/forge/game/card/CardView.java | 4 +- .../res/cardsfolder/c/clown_extruder.txt | 10 +++ .../res/cardsfolder/c/concession_stand.txt | 10 +++ .../res/cardsfolder/e/everythingamajig.txt | 10 +++ .../res/cardsfolder/g/garbage_elemental.txt | 16 +++++ forge-gui/res/cardsfolder/s/sly_spy.txt | 9 +++ .../res/cardsfolder/s/squirrel_squatters.txt | 11 +++ forge-gui/res/editions/Unstable.txt | 72 +++++++++---------- 14 files changed, 136 insertions(+), 60 deletions(-) create mode 100644 forge-gui/res/cardsfolder/c/clown_extruder.txt create mode 100644 forge-gui/res/cardsfolder/c/concession_stand.txt create mode 100644 forge-gui/res/cardsfolder/e/everythingamajig.txt create mode 100644 forge-gui/res/cardsfolder/g/garbage_elemental.txt create mode 100644 forge-gui/res/cardsfolder/s/sly_spy.txt create mode 100644 forge-gui/res/cardsfolder/s/squirrel_squatters.txt diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java index afee93adf4b..3ae67fddc5d 100644 --- a/forge-core/src/main/java/forge/card/CardEdition.java +++ b/forge-core/src/main/java/forge/card/CardEdition.java @@ -577,7 +577,7 @@ protected CardEdition read(File file) { * 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(); diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 1dc538ead80..49ee3d67ffa 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -2649,6 +2649,10 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { return game.getPhaseHandler().getPlanarDiceSpecialActionThisTurn(); } + if (sq[0].startsWith("AttractionsYouVisitedThisTurn")) { + return doXMath(player.getAttractionsVisitedThisTurn(), expr, c, ctb); + } + if (sq[0].equals("AllTypes")) { List cards = getDefinedCards(c, sq[1], ctb); 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 08555f6345e..57b01dd5670 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 @@ -221,6 +221,12 @@ private int rollDice(SpellAbility sa, Player player, int amount, int sides) { List rolls = new ArrayList<>(); int total = rollDiceForPlayer(sa, player, amount, sides, ignore, modifier, rolls); + if (sa.hasParam("UseHighestRoll")) { + total = Collections.max(rolls); + } else if (sa.hasParam("UseDifferenceBetweenRolls")) { + total = Collections.max(rolls) - Collections.min(rolls); + } + if (sa.hasParam("StoreResults")) { host.addStoredRolls(rolls); } @@ -243,9 +249,6 @@ private int rollDice(SpellAbility sa, Player player, int amount, int sides) { sa.setSVar(sa.getParam("OtherSVar"), Integer.toString(other)); } } - if (sa.hasParam("UseHighestRoll")) { - total = Collections.max(rolls); - } if (sa.hasParam("SubsForEach")) { for (Integer roll : rolls) { 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 e5871c6fffc..4c0d1d085a2 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -277,8 +277,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private Table> newPT = TreeBasedTable.create(); // Layer 7b private Table> boostPT = TreeBasedTable.create(); // Layer 7c - private String oracleText = ""; - private final Map assignedDamageMap = Maps.newTreeMap(); private Map damage = Maps.newHashMap(); private boolean hasBeenDealtDeathtouchDamage; @@ -2506,7 +2504,7 @@ public final String keywordsToText(final Collection keywords) || keyword.startsWith("Class") || keyword.startsWith("Blitz") || keyword.startsWith("Specialize") || keyword.equals("Ravenous") || keyword.equals("For Mirrodin") || keyword.startsWith("Craft") - || keyword.startsWith("Landwalk")) { + || keyword.startsWith("Landwalk") || keyword.startsWith("Visit")) { // keyword parsing takes care of adding a proper description } else if (keyword.equals("Read ahead")) { sb.append(Localizer.getInstance().getMessage("lblReadAhead")).append(" (").append(Localizer.getInstance().getMessage("lblReadAheadDesc")); @@ -3498,7 +3496,7 @@ public final Card getCopiedPermanent() { public final void setCopiedPermanent(final Card c) { if (copiedPermanent == c) { return; } copiedPermanent = c; - currentState.getView().updateOracleText(this); + currentState.setOracleText(c.getOracleText()); } public final boolean isCopiedSpell() { @@ -7244,7 +7242,6 @@ public CardRules getRules() { public void setRules(CardRules r) { cardRules = r; currentState.getView().updateRulesText(r, getType()); - currentState.getView().updateOracleText(this); } public boolean isCommander() { @@ -7573,15 +7570,10 @@ public void addStaticCommandList(Object[] objects) { } public String getOracleText() { - CardRules rules = cardRules; - if (copiedPermanent != null) { //return oracle text of copied permanent if applicable - rules = copiedPermanent.getRules(); - } - return rules != null ? rules.getOracleText() : oracleText; + return currentState.getOracleText(); } - public void setOracleText(final String oracleText0) { - oracleText = oracleText0; - currentState.getView().updateOracleText(this); + public void setOracleText(final String oracleText) { + currentState.setOracleText(oracleText); } @Override 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 a2411251419..100adcc6030 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -346,9 +346,9 @@ private static Card readCard(final CardRules rules, final IPaperCard paperCard, card.setColor(combinedColor); card.setType(new CardType(rules.getType())); - // Combined text based on Oracle text - might not be necessary, temporarily disabled. - //String combinedText = String.format("%s: %s\n%s: %s", rules.getMainPart().getName(), rules.getMainPart().getOracleText(), rules.getOtherPart().getName(), rules.getOtherPart().getOracleText()); - //card.setText(combinedText); + // Combined text based on Oracle text - might not be necessary + String combinedText = String.format("(%s) %s\r\n\r\n(%s) %s", rules.getMainPart().getName(), rules.getMainPart().getOracleText(), rules.getOtherPart().getName(), rules.getOtherPart().getOracleText()); + card.getState(CardStateName.Original).setOracleText(combinedText); } return card; } @@ -377,7 +377,7 @@ private static void readCardFace(Card c, ICardFace face) { c.getCurrentState().setBaseLoyalty(face.getInitialLoyalty()); c.getCurrentState().setBaseDefense(face.getDefense()); - c.setOracleText(face.getOracleText()); + c.getCurrentState().setOracleText(face.getOracleText()); // Super and 'middle' types should use enums. c.setType(new CardType(face.getType())); @@ -454,7 +454,7 @@ private static void applyFunctionalVariant(Card c, ICardFace originalFace) { c.getCurrentState().setBaseDefense(variant.getDefense()); if (variant.getOracleText() != null) - c.setOracleText(variant.getOracleText()); + c.getCurrentState().setOracleText(variant.getOracleText()); if (variant.getType() != null) { for(String type : variant.getType()) 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 90c1303228d..895b92b37ed 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -58,6 +58,7 @@ public class CardState extends GameObject implements IHasSVars { private CardType type = new CardType(false); private ManaCost manaCost = ManaCost.NO_COST; private byte color = MagicColor.COLORLESS; + private String oracleText = ""; private int basePower = 0; private int baseToughness = 0; private String basePowerString = null; @@ -194,6 +195,15 @@ public final void setColor(final byte color) { view.updateColors(card); } + public String getOracleText() { + return oracleText; + } + public void setOracleText(final String oracleText) { + this.oracleText = oracleText; + view.setOracleText(oracleText); + } + + public final int getBasePower() { return basePower; } @@ -595,6 +605,7 @@ public final void copyFrom(final CardState source, final boolean lki, final Card setType(source.type); setManaCost(source.getManaCost()); setColor(source.getColor()); + setOracleText(source.getOracleText()); setBasePower(source.getBasePower()); setBaseToughness(source.getBaseToughness()); setBaseLoyalty(source.getBaseLoyalty()); 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 21ca73a61ff..de32b0573c7 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -1298,8 +1298,8 @@ void updateManaCost(Card c) { public String getOracleText() { return get(TrackableProperty.OracleText); } - void updateOracleText(Card c) { - set(TrackableProperty.OracleText, c.getOracleText().replace("\\n", "\r\n\r\n").trim()); + void setOracleText(String oracleText) { + set(TrackableProperty.OracleText, oracleText.replace("\\n", "\r\n\r\n").trim()); } public String getRulesText() { diff --git a/forge-gui/res/cardsfolder/c/clown_extruder.txt b/forge-gui/res/cardsfolder/c/clown_extruder.txt new file mode 100644 index 00000000000..fc52d1f353c --- /dev/null +++ b/forge-gui/res/cardsfolder/c/clown_extruder.txt @@ -0,0 +1,10 @@ +Name:Clown Extruder +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:TrigToken +SVar:TrigToken:DB$ Token | TokenScript$ w_1_1_a_clown_robot | TokenOwner$ You | SpellDescription$ Create a 1/1 white Clown Robot artifact creature token. +Oracle:Visit — Create a 1/1 white Clown Robot artifact creature token. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/c/concession_stand.txt b/forge-gui/res/cardsfolder/c/concession_stand.txt new file mode 100644 index 00000000000..cec38a48620 --- /dev/null +++ b/forge-gui/res/cardsfolder/c/concession_stand.txt @@ -0,0 +1,10 @@ +Name:Concession Stand +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:TrigFood +SVar:TrigFood:DB$ Token | TokenScript$ c_a_food_sac | TokenOwner$ You | SpellDescription$ Create a Food token. +Oracle:Visit — Create a Food token. (It’s an artifact with “{2}, {T}, Sacrifice this artifact: You gain 3 life.”) \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/e/everythingamajig.txt b/forge-gui/res/cardsfolder/e/everythingamajig.txt new file mode 100644 index 00000000000..ecc98832f31 --- /dev/null +++ b/forge-gui/res/cardsfolder/e/everythingamajig.txt @@ -0,0 +1,10 @@ +Name:Everythingamajig +ManaCost:5 +Types:Artifact +Variant:C:A:AB$ FlipACoin | Cost$ 1 | WinSubAbility$ DBAddMana | InstantSpeed$ True | SpellDescription$ Flip a coin. If you win the flip, add {C}{C}. +Variant:C:SVar:DBAddMana:DB$ Mana | Produced$ C | Amount$ 2 +Variant:C:A:AB$ Discard | Cost$ 3 T | ValidTgts$ Player | NumCards$ 1 | Mode$ TgtChoose | PlayerTurn$ True | SpellDescription$ Target player discards a card. +Variant:C:A:AB$ Animate | Cost$ X | Defined$ Self | Power$ X | Toughness$ X | Types$ Creature,Artifact,Construct | RemoveCreatureTypes$ True | SpellDescription$ CARDNAME becomes an X/X Construct artifact creature until end of turn. +Variant:C:SVar:X:Count$xPaid +Oracle: +Variant:C:Oracle:{1}: Flip a coin. If you win the flip, add {C}{C}. Activate only as an instant.\n{3}, {T}: Target player discards a card. Activate only during your turn.\n{X}: Everythingamajig becomes an X/X Construct artifact creature until end of turn. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/g/garbage_elemental.txt b/forge-gui/res/cardsfolder/g/garbage_elemental.txt new file mode 100644 index 00000000000..534aa324f76 --- /dev/null +++ b/forge-gui/res/cardsfolder/g/garbage_elemental.txt @@ -0,0 +1,16 @@ +Name:Garbage Elemental +ManaCost:4 R +Types:Creature Elemental +Variant:C:PT:3/2 +Variant:C:K:Battle cry +Variant:C:T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigRoll | TriggerDescription$ When CARDNAME enters the battlefield, roll two six-sided dice. Create a number of 1/1 red Goblin creature tokens equal to the difference between those results. +Variant:C:SVar:TrigRoll:DB$ RollDice | ResultSVar$ Result | Sides$ 6 | Amount$ 2 | UseDifferenceBetweenRolls$ True | SubAbility$ DBToken +Variant:C:SVar:DBToken:DB$ Token | TokenScript$ r_1_1_goblin | TokenAmount$ Result +Variant:D:PT:3/3 +Variant:D:K:Cascade +Variant:D:T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigRoll | TriggerDescription$ When CARDNAME enters the battlefield, roll a six-sided die. CARDNAME deals damage equal to the result to target opponent or planeswalker. +Variant:D:SVar:TrigRoll:DB$ RollDice | ResultSVar$ Result | SubAbility$ DBDamage +Variant:D:SVar:DBDamage:DB$ DealDamage | ValidTgts$ Opponent,Planeswalker | TgtPrompt$ Select target opponent or planeswalker | NumDmg$ Result +Oracle: +Variant:C:Oracle:Battle cry (Whenever this creature attacks, each other attacking creature gets +1/+0 until end of turn.)\nWhen Garbage Elemental enters the battlefield, roll two six-sided dice. Create a number of 1/1 red Goblin creature tokens equal to the difference between those results. +Variant:D:Oracle:Cascade (When you cast this spell, exile cards from the top of your library until you exile a nonland card that costs less. You may cast it without paying its mana cost. Put the exiled cards on the bottom of your library in a random order.)\nWhen Garbage Elemental enters the battlefield, roll a six-sided die. Garbage Elemental deals damage equal to the result to target opponent or planeswalker. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/s/sly_spy.txt b/forge-gui/res/cardsfolder/s/sly_spy.txt new file mode 100644 index 00000000000..981f8e0114a --- /dev/null +++ b/forge-gui/res/cardsfolder/s/sly_spy.txt @@ -0,0 +1,9 @@ +Name:Sly Spy +ManaCost:2 B +Types:Creature Human Spy +PT:2/2 +Variant:F:T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigRoll | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, roll a six-sided die. That player loses life equal to the result. +Variant:F:SVar:TrigRoll:DB$ RollDice | ResultSVar$ Result | SubAbility$ DBLoseLife +Variant:F:SVar:DBLoseLife:DB$ LoseLife | Defined$ TriggeredTarget | LifeAmount$ Result +Oracle: +Variant:F:Oracle:Whenever Sly Spy deals combat damage to a player, roll a six-sided die. That player loses life equal to the result. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/s/squirrel_squatters.txt b/forge-gui/res/cardsfolder/s/squirrel_squatters.txt new file mode 100644 index 00000000000..855c49333bc --- /dev/null +++ b/forge-gui/res/cardsfolder/s/squirrel_squatters.txt @@ -0,0 +1,11 @@ +Name:Squirrel Squatters +ManaCost:3 G G +Types:Creature Squirrel +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 +T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigToken | TriggerDescription$ Whenever CARDNAME attacks, create a 1/1 green Squirrel creature token that's tapped and attacking for each Attraction you've visited this turn. +SVar:TrigToken:DB$ Token | TokenAmount$ X | TokenScript$ g_1_1_squirrel | TokenOwner$ You | TokenTapped$ True | TokenAttacking$ True +SVar:X:Count$AttractionsYouVisitedThisTurn +SVar:HasAttackEffect:TRUE +Oracle:When Squirrel Squatters enters the battlefield, open an Attraction. (Put the top card of your Attraction deck onto the battlefield.)\nWhenever Squirrel Squatters attacks, create a 1/1 green Squirrel creature token that’s tapped and attacking for each Attraction you’ve visited this turn. \ No newline at end of file diff --git a/forge-gui/res/editions/Unstable.txt b/forge-gui/res/editions/Unstable.txt index 999424045dd..cd1b746fc20 100644 --- a/forge-gui/res/editions/Unstable.txt +++ b/forge-gui/res/editions/Unstable.txt @@ -20,12 +20,12 @@ ScryfallCode=UST 9 U Half-Kitten, Half- 10 C Humming- 11 R Jackknight -12a U Knight of the Kitchen Sink A -12b U Knight of the Kitchen Sink B -12c U Knight of the Kitchen Sink C -12d U Knight of the Kitchen Sink D -12e U Knight of the Kitchen Sink E -12f U Knight of the Kitchen Sink F +12a U Knight of the Kitchen Sink $A +12b U Knight of the Kitchen Sink $B +12c U Knight of the Kitchen Sink $C +12d U Knight of the Kitchen Sink $D +12e U Knight of the Kitchen Sink $E +12f U Knight of the Kitchen Sink $F 13 U Knight of the Widget 14 U Midlife Upgrade 15 R Oddly Uneven @@ -65,12 +65,12 @@ ScryfallCode=UST 46 U Spy Eye 47 U Suspicious Nanny 48 C Time Out -49a R Very Cryptic Command A -49b R Very Cryptic Command B -49c R Very Cryptic Command C -49d R Very Cryptic Command D -49e R Very Cryptic Command E -49f R Very Cryptic Command F +49a R Very Cryptic Command $A +49b R Very Cryptic Command $B +49c R Very Cryptic Command $C +49d R Very Cryptic Command $D +49e R Very Cryptic Command $E +49f R Very Cryptic Command $F 50 C Wall of Fortune 51 C Big Boa Constrictor 52 C capital offense @@ -91,12 +91,12 @@ ScryfallCode=UST 64 U Overt Operative 65 U "Rumors of My Death..." 66 U Skull Saucer -67a U Sly Spy A -67b U Sly Spy B -67c U Sly Spy C -67d U Sly Spy D -67e U Sly Spy E -67f U Sly Spy F +67a U Sly Spy $A +67b U Sly Spy $B +67c U Sly Spy $C +67d U Sly Spy $D +67e U Sly Spy $E +67f U Sly Spy $F 68 C Snickering Squirrel 69 R Spike, Tournament Grinder 70 U Squirrel-Powered Scheme @@ -111,12 +111,12 @@ ScryfallCode=UST 79 C Common Iguana 80 R The Countdown Is at One 81 C Feisty Stegosaurus -82a U Garbage Elemental A -82b U Garbage Elemental B -82c U Garbage Elemental C -82d U Garbage Elemental D -82e U Garbage Elemental E -82f U Garbage Elemental F +82a U Garbage Elemental $A +82b U Garbage Elemental $B +82c U Garbage Elemental $C +82d U Garbage Elemental $D +82e U Garbage Elemental $E +82f U Garbage Elemental $F 83 U Goblin Haberdasher 84 U Half-Orc, Half- 85 C Hammer Helper @@ -153,12 +153,12 @@ ScryfallCode=UST 110 U Ground Pounder 111 U Half-Squirrel, Half- 112 R Hydradoodle -113a R Ineffable Blessing A -113b R Ineffable Blessing B -113c R Ineffable Blessing C -113d R Ineffable Blessing D -113e R Ineffable Blessing E -113f R Ineffable Blessing F +113a R Ineffable Blessing $A +113b R Ineffable Blessing $B +113c R Ineffable Blessing $C +113d R Ineffable Blessing $D +113e R Ineffable Blessing $E +113f R Ineffable Blessing $F 114 C Joyride Rigger 115 U Monkey- 116 C Mother Kangaroo @@ -195,12 +195,12 @@ ScryfallCode=UST 145c C Despondent Killbot 145d C Enraged Killbot 146 U Entirely Normal Armchair -147a R Everythingamajig A -147b R Everythingamajig B -147c R Everythingamajig C -147d R Everythingamajig D -147e R Everythingamajig E -147f R Everythingamajig F +147a R Everythingamajig $A +147b R Everythingamajig $B +147c R Everythingamajig $C +147d R Everythingamajig $D +147e R Everythingamajig $E +147f R Everythingamajig $F 148 C Gnome-Made Engine 149 R Handy Dandy Clone Machine 150 R Kindslaver From 279e1e520ef6c17c1d381d0d5621db03feb0aaa8 Mon Sep 17 00:00:00 2001 From: Jetz Date: Fri, 7 Jun 2024 21:59:59 -0400 Subject: [PATCH 05/20] Disambiguate requested print names --- forge-core/src/main/java/forge/card/CardDb.java | 16 +++++++++------- .../src/main/java/forge/util/ImageUtil.java | 5 ++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java index 787b5742c04..85a354f57f5 100644 --- a/forge-core/src/main/java/forge/card/CardDb.java +++ b/forge-core/src/main/java/forge/card/CardDb.java @@ -888,14 +888,16 @@ public int getMaxArtIndex(String cardName) { @Override public int getArtCount(String cardName, String setCode) { + return getArtCount(cardName, setCode, null); + } + public int getArtCount(String cardName, String setCode, String functionalVariantName) { if (cardName == null || setCode == null) return 0; - Collection cardsInSet = getAllCards(cardName, new Predicate() { - @Override - public boolean apply(PaperCard card) { - return card.getEdition().equalsIgnoreCase(setCode); - } - }); + Predicate predicate = card -> card.getEdition().equalsIgnoreCase(setCode); + if(functionalVariantName != null && !functionalVariantName.equals(IPaperCard.NO_FUNCTIONAL_VARIANT)) { + predicate = Predicates.and(predicate, card -> functionalVariantName.equals(card.getFunctionalVariant())); + } + Collection cardsInSet = getAllCards(cardName, predicate); return cardsInSet.size(); } @@ -1159,7 +1161,7 @@ public StringBuilder appendCardToStringBuilder(PaperCard card, StringBuilder sb) } if (!hasBadSetInfo) { - int artCount = getArtCount(card.getName(), card.getEdition()); + 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 diff --git a/forge-core/src/main/java/forge/util/ImageUtil.java b/forge-core/src/main/java/forge/util/ImageUtil.java index e3bfaf4dacf..bf535e91207 100644 --- a/forge-core/src/main/java/forge/util/ImageUtil.java +++ b/forge-core/src/main/java/forge/util/ImageUtil.java @@ -5,6 +5,7 @@ import forge.card.CardDb; import forge.card.CardRules; import forge.card.CardSplitType; +import forge.item.IPaperCard; import forge.item.PaperCard; import org.apache.commons.lang3.StringUtils; @@ -74,7 +75,7 @@ public static String getImageRelativePath(PaperCard cp, String face, boolean inc final boolean hasManyPictures; final CardDb db = !card.isVariant() ? StaticData.instance().getCommonCards() : StaticData.instance().getVariantCards(); if (includeSet) { - cntPictures = db.getArtCount(card.getName(), edition); + cntPictures = db.getArtCount(card.getName(), edition, cp.getFunctionalVariant()); hasManyPictures = cntPictures > 1; } else { cntPictures = 1; @@ -149,6 +150,8 @@ public static String getNameToUse(PaperCard cp, String face) { } } else if (CardSplitType.Split == cp.getRules().getSplitType()) { return card.getMainPart().getName() + card.getOtherPart().getName(); + } else if (!IPaperCard.NO_FUNCTIONAL_VARIANT.equals(cp.getFunctionalVariant())) { + return cp.getName() + " " + cp.getFunctionalVariant(); } return cp.getName(); } From a3662079b4f2bba3f6d4548681a932787cb13623 Mon Sep 17 00:00:00 2001 From: Jetz Date: Fri, 7 Jun 2024 22:58:09 -0400 Subject: [PATCH 06/20] Fix potential NPE. --- forge-game/src/main/java/forge/game/card/Card.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 6bcd9f76125..849f91f953a 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -3484,7 +3484,11 @@ public final Card getCopiedPermanent() { public final void setCopiedPermanent(final Card c) { if (copiedPermanent == c) { return; } copiedPermanent = c; - currentState.setOracleText(c.getOracleText()); + if(c != null) + currentState.setOracleText(c.getOracleText()); + //Could fetch the card rules oracle text in an "else" clause here, + //but CardRules isn't aware of the card's state. May be better to + //just stash the original oracle text if this comes up. } public final boolean isCopiedSpell() { From 1e5e1ba6c0e80f1cc87c8c50b7d6b524a41dde3d Mon Sep 17 00:00:00 2001 From: Jetz Date: Sat, 8 Jun 2024 12:36:20 -0400 Subject: [PATCH 07/20] Redundant import --- forge-core/src/main/java/forge/card/CardRules.java | 1 - 1 file changed, 1 deletion(-) diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java index c656ca7ef6d..8c9f110fb93 100644 --- a/forge-core/src/main/java/forge/card/CardRules.java +++ b/forge-core/src/main/java/forge/card/CardRules.java @@ -23,7 +23,6 @@ import org.apache.commons.lang3.StringUtils; import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.card.mana.IParserManaCost; From 614cc96f4f271d750d6781131a1e6c78be9b8d42 Mon Sep 17 00:00:00 2001 From: Jetz Date: Sun, 9 Jun 2024 13:31:04 -0400 Subject: [PATCH 08/20] Support for adding Tabs to TabPageScreen --- .../adventure/scene/AdventureDeckEditor.java | 2 +- .../src/forge/card/GameEntityPicker.java | 2 +- .../src/forge/deck/FDeckEditor.java | 2 +- .../src/forge/deck/FSideboardDialog.java | 8 +-- .../src/forge/screens/TabPageScreen.java | 56 ++++++++++++------- .../ConquestCollectionScreen.java | 8 +-- .../screens/quest/QuestSpellShopScreen.java | 4 +- .../screens/settings/SettingsScreen.java | 2 +- 8 files changed, 51 insertions(+), 33 deletions(-) diff --git a/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java b/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java index 3d3fbd441b5..5e586a8c875 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java +++ b/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java @@ -1405,7 +1405,7 @@ else if (getSelectedPage() instanceof DeckSectionPage) selected++; if (selected > 2) selected = 0; - setSelectedPage(tabPages[selected]); + setSelectedPage(tabPages.get(selected)); if (getSelectedPage() instanceof CatalogPage) { ((CatalogPage) getSelectedPage()).cardManager.getConfig().setPileBy(null); ((CatalogPage) getSelectedPage()).cardManager.setHideFilters(true); diff --git a/forge-gui-mobile/src/forge/card/GameEntityPicker.java b/forge-gui-mobile/src/forge/card/GameEntityPicker.java index 22aa5cb889f..2591e8a670e 100644 --- a/forge-gui-mobile/src/forge/card/GameEntityPicker.java +++ b/forge-gui-mobile/src/forge/card/GameEntityPicker.java @@ -38,7 +38,7 @@ public GameEntityPicker(String title, Collection choic @Override public void run(Integer result) { if (result == 0) { - callback.run(((PickerTab)tabPages[0]).list.getSelectedItem()); + callback.run(((PickerTab) tabPages.get(0)).list.getSelectedItem()); } else { callback.run(null); diff --git a/forge-gui-mobile/src/forge/deck/FDeckEditor.java b/forge-gui-mobile/src/forge/deck/FDeckEditor.java index 4886c39bedc..e1b900e8e8e 100644 --- a/forge-gui-mobile/src/forge/deck/FDeckEditor.java +++ b/forge-gui-mobile/src/forge/deck/FDeckEditor.java @@ -346,7 +346,7 @@ else if (tabPage instanceof DeckSectionPage) { } } else { if (editorType == EditorType.Draft || editorType == EditorType.QuestDraft) { - tabPages[0].hideTab(); //hide Draft Pack page if editing existing draft deck + tabPages.get(0).hideTab(); //hide Draft Pack page if editing existing draft deck } editorType.getController().load(editDeckPath, editDeckName); } diff --git a/forge-gui-mobile/src/forge/deck/FSideboardDialog.java b/forge-gui-mobile/src/forge/deck/FSideboardDialog.java index 3255d779db8..3eb606719b4 100644 --- a/forge-gui-mobile/src/forge/deck/FSideboardDialog.java +++ b/forge-gui-mobile/src/forge/deck/FSideboardDialog.java @@ -60,16 +60,16 @@ private SideboardTabs(CardPool sideboard, CardPool main) { new SideboardPage(sideboard), new MainDeckPage(main) }, false); - ((SideboardPage)tabPages[0]).parent = this; - ((MainDeckPage)tabPages[1]).parent = this; + ((SideboardPage) tabPages.get(0)).parent = this; + ((MainDeckPage) tabPages.get(1)).parent = this; } private SideboardPage getSideboardPage() { - return ((SideboardPage)tabPages[0]); + return ((SideboardPage) tabPages.get(0)); } private MainDeckPage getMainDeckPage() { - return ((MainDeckPage)tabPages[1]); + return ((MainDeckPage) tabPages.get(1)); } @Override diff --git a/forge-gui-mobile/src/forge/screens/TabPageScreen.java b/forge-gui-mobile/src/forge/screens/TabPageScreen.java index 72ac5064f08..0be8ee16028 100644 --- a/forge-gui-mobile/src/forge/screens/TabPageScreen.java +++ b/forge-gui-mobile/src/forge/screens/TabPageScreen.java @@ -18,11 +18,14 @@ import forge.toolbox.FScrollPane; import forge.util.Utils; +import java.util.ArrayList; +import java.util.List; + public class TabPageScreen> extends FScreen { public static boolean COMPACT_TABS = FModel.getPreferences().getPrefBoolean(FPref.UI_COMPACT_TABS); protected final TabHeader tabHeader; - protected final TabPage[] tabPages; + protected final List> tabPages; private TabPage selectedPage; @SuppressWarnings("unchecked") @@ -72,7 +75,16 @@ private void initialize() { add(tabPage); tabPage.setVisible(false); } - setSelectedPage(tabPages[0]); + setSelectedPage(tabPages.get(0)); + } + + @SuppressWarnings("unchecked") + public void addTabPage(TabPage tabPage) { + tabHeader.addTab(tabPage); + tabPage.index = tabPages.size(); + tabPage.parentScreen = (T) this; + add(tabPage); + tabPage.setVisible(false); } public TabPage getSelectedPage() { @@ -135,7 +147,7 @@ protected static class TabHeader> extends Header { private static final float BACK_BUTTON_WIDTH = Math.round(HEIGHT / 2); private static final FSkinColor SEPARATOR_COLOR = getBackColor().stepColor(-40); - private final TabPage[] tabPages; + private final List> tabPages = new ArrayList<>(); public final FLabel btnBack; private boolean isScrollable; private FDisplayObject finalVisibleTab; @@ -184,8 +196,7 @@ protected ScrollBounds layoutAndGetScrollBounds(float visibleWidth, float visibl } }); - public TabHeader(TabPage[] tabPages0, boolean showBackButton) { - tabPages = tabPages0; + public TabHeader(TabPage[] tabPages, boolean showBackButton) { if (showBackButton) { btnBack = add(new FLabel.Builder().icon(new BackIcon(BACK_BUTTON_WIDTH, BACK_BUTTON_WIDTH)).pressedColor(getBtnPressedColor()).align(Align.center).command(e -> Forge.back()).build()); } @@ -194,11 +205,11 @@ public TabHeader(TabPage[] tabPages0, boolean showBackButton) { } for (TabPage tabPage : tabPages) { - scroller.add(tabPage.tab); + this.tabPages.add(tabPage); + this.scroller.add(tabPage.tab); } } - public TabHeader(TabPage[] tabPages0, FEventHandler backButton) { - tabPages = tabPages0; + public TabHeader(TabPage[] tabPages, FEventHandler backButton) { if(backButton==null) { btnBack = add(new FLabel.Builder().icon(new BackIcon(BACK_BUTTON_WIDTH, BACK_BUTTON_WIDTH)).pressedColor(getBtnPressedColor()).align(Align.center).command(e -> Forge.back()).build()); } @@ -208,16 +219,23 @@ public TabHeader(TabPage[] tabPages0, FEventHandler backButton) { } for (TabPage tabPage : tabPages) { - scroller.add(tabPage.tab); + this.tabPages.add(tabPage); + this.scroller.add(tabPage.tab); } } + + public void addTab(TabPage tabPage) { + this.tabPages.add(tabPage); + this.scroller.add(tabPage.tab); + } + protected boolean showBackButtonInLandscapeMode() { return btnBack != null; } @Override public float getPreferredHeight() { - return tabPages[0].parentScreen.showCompactTabs() ? COMPACT_HEIGHT : HEIGHT; + return tabPages.get(0).parentScreen.showCompactTabs() ? COMPACT_HEIGHT : HEIGHT; } @Override @@ -262,7 +280,7 @@ protected void doLayout(float width, float height) { } } else { - btnBack.setIconScaleAuto(tabPages[0].parentScreen.showCompactTabs()); + btnBack.setIconScaleAuto(tabPages.get(0).parentScreen.showCompactTabs()); btnBack.setSize(BACK_BUTTON_WIDTH, height); x += BACK_BUTTON_WIDTH; } @@ -326,12 +344,12 @@ public boolean fling(float velocityX, float velocityY) { //switch to next/previous tab page when flung left or right if (Math.abs(velocityX) > Math.abs(velocityY)) { if (velocityX < 0) { - if (index < parentScreen.tabPages.length - 1) { - parentScreen.setSelectedPage(parentScreen.tabPages[index + 1]); + if (index < parentScreen.tabPages.size() - 1) { + parentScreen.setSelectedPage(parentScreen.tabPages.get(index + 1)); } } else if (index > 0) { - parentScreen.setSelectedPage(parentScreen.tabPages[index - 1]); + parentScreen.setSelectedPage(parentScreen.tabPages.get(index - 1)); } return true; } @@ -352,16 +370,16 @@ public void setVisible(boolean b0) { if (!b0 && parentScreen.getSelectedPage() == TabPage.this) { //select next page if this page is hidden - for (int i = index + 1; i < parentScreen.tabPages.length; i++) { - if (parentScreen.tabPages[i].tab.isVisible()) { - parentScreen.setSelectedPage(parentScreen.tabPages[i]); + for (int i = index + 1; i < parentScreen.tabPages.size(); i++) { + if (parentScreen.tabPages.get(i).tab.isVisible()) { + parentScreen.setSelectedPage(parentScreen.tabPages.get(i)); return; } } //select previous page if selecting next page is not possible for (int i = index - 1; i >= 0; i--) { - if (parentScreen.tabPages[i].tab.isVisible()) { - parentScreen.setSelectedPage(parentScreen.tabPages[i]); + if (parentScreen.tabPages.get(i).tab.isVisible()) { + parentScreen.setSelectedPage(parentScreen.tabPages.get(i)); return; } } diff --git a/forge-gui-mobile/src/forge/screens/planarconquest/ConquestCollectionScreen.java b/forge-gui-mobile/src/forge/screens/planarconquest/ConquestCollectionScreen.java index 8f335335cf5..c8655e38882 100644 --- a/forge-gui-mobile/src/forge/screens/planarconquest/ConquestCollectionScreen.java +++ b/forge-gui-mobile/src/forge/screens/planarconquest/ConquestCollectionScreen.java @@ -60,7 +60,7 @@ public void handleEvent(FEvent e) { FThreads.invokeInBackgroundThread(new Runnable() { @Override public void run() { - if (getSelectedPage() == tabPages[0]) { + if (getSelectedPage() == tabPages.get(0)) { int value = 0; for (PaperCard card : cards) { value += ConquestUtil.getShardValue(card, CQPref.AETHER_BASE_EXILE_VALUE); @@ -143,7 +143,7 @@ private void updateExileRetrieveButtonCaption() { String caption; CQPref baseValuePref; Collection cards; - if (getSelectedPage() == tabPages[0]) { + if (getSelectedPage() == tabPages.get(0)) { caption = Forge.getLocalizer().getMessage("lblExile"); baseValuePref = CQPref.AETHER_BASE_EXILE_VALUE; cards = getCollectionTab().list.getSelectedItems(); @@ -172,11 +172,11 @@ private void updateExileRetrieveButtonCaption() { } private CollectionTab getCollectionTab() { - return (CollectionTab)tabPages[0]; + return (CollectionTab) tabPages.get(0); } private CollectionTab getExileTab() { - return (CollectionTab)tabPages[1]; + return (CollectionTab) tabPages.get(1); } @Override diff --git a/forge-gui-mobile/src/forge/screens/quest/QuestSpellShopScreen.java b/forge-gui-mobile/src/forge/screens/quest/QuestSpellShopScreen.java index 4cf53a0909f..0cc8812fd74 100644 --- a/forge-gui-mobile/src/forge/screens/quest/QuestSpellShopScreen.java +++ b/forge-gui-mobile/src/forge/screens/quest/QuestSpellShopScreen.java @@ -43,8 +43,8 @@ public class QuestSpellShopScreen extends TabPageScreen { public QuestSpellShopScreen() { super("", QuestMenu.getMenu(), new SpellShopBasePage[] { new SpellShopPage(), new InventoryPage() }, true); - spellShopPage = ((SpellShopPage)tabPages[0]); - inventoryPage = ((InventoryPage)tabPages[1]); + spellShopPage = ((SpellShopPage) tabPages.get(0)); + inventoryPage = ((InventoryPage) tabPages.get(1)); btnBuySellMultiple.setVisible(false); //hide unless in multi-select mode btnBuySellMultiple.setCommand(event -> { diff --git a/forge-gui-mobile/src/forge/screens/settings/SettingsScreen.java b/forge-gui-mobile/src/forge/screens/settings/SettingsScreen.java index 6bfc3881d01..69c74dbc78c 100644 --- a/forge-gui-mobile/src/forge/screens/settings/SettingsScreen.java +++ b/forge-gui-mobile/src/forge/screens/settings/SettingsScreen.java @@ -60,7 +60,7 @@ protected boolean showBackButtonInLandscapeMode() { return !fromHomeScreen; //don't show back button if launched from home screen } }); - settingsPage = (SettingsPage) tabPages[0]; + settingsPage = (SettingsPage) tabPages.get(0); } public FScreen getLandscapeBackdropScreen() { From 239735eaca13b17ddbefe8a9cf7c7b628dd2b26c Mon Sep 17 00:00:00 2001 From: Jetz Date: Sat, 15 Jun 2024 12:39:38 -0400 Subject: [PATCH 09/20] Make countByName work for cards with multiple prints. Add countByName to Deck --- forge-core/src/main/java/forge/deck/CardPool.java | 10 +++++----- forge-core/src/main/java/forge/deck/Deck.java | 11 +++++++++++ forge-core/src/main/java/forge/deck/DeckFormat.java | 8 ++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/forge-core/src/main/java/forge/deck/CardPool.java b/forge-core/src/main/java/forge/deck/CardPool.java index e1029521e9f..8e7d9a531e4 100644 --- a/forge-core/src/main/java/forge/deck/CardPool.java +++ b/forge-core/src/main/java/forge/deck/CardPool.java @@ -155,12 +155,12 @@ public PaperCard get(int n) { return null; } - public int countByName(String cardName, boolean isCommonCard) { - PaperCard pc = isCommonCard - ? StaticData.instance().getCommonCards().getCard(cardName) - : StaticData.instance().getVariantCards().getCard(cardName); + public int countByName(String cardName) { + return this.countAll((c) -> c.getName().equals(cardName)); + } - return this.count(pc); + public int countByName(PaperCard card) { + return this.countAll((c) -> c.getName().equals(card.getName())); } /** diff --git a/forge-core/src/main/java/forge/deck/Deck.java b/forge-core/src/main/java/forge/deck/Deck.java index 7c8fa7ed594..913a2c5a264 100644 --- a/forge-core/src/main/java/forge/deck/Deck.java +++ b/forge-core/src/main/java/forge/deck/Deck.java @@ -537,6 +537,17 @@ public CardPool getAllCardsInASinglePool(final boolean includeCommander) { return allCards; } + /** + * Counts the number of cards with the given name across all deck sections. + */ + public int countByName(String cardName) { + int sum = 0; + for (Entry section : this) { + sum += section.getValue().countByName(cardName); + } + return sum; + } + public void setAiHints(String aiHintsInfo) { if (aiHintsInfo == null || aiHintsInfo.trim().equals("")) { return; diff --git a/forge-core/src/main/java/forge/deck/DeckFormat.java b/forge-core/src/main/java/forge/deck/DeckFormat.java index 4302e19604b..6dedb34888f 100644 --- a/forge-core/src/main/java/forge/deck/DeckFormat.java +++ b/forge-core/src/main/java/forge/deck/DeckFormat.java @@ -217,8 +217,8 @@ public String getDeckConformanceProblem(Deck deck) { // Adjust minimum base on number of Advantageous Proclamation or similar cards CardPool conspiracies = deck.get(DeckSection.Conspiracy); if (conspiracies != null) { - min -= (5 * conspiracies.countByName(ADVPROCLAMATION, false)); - noBasicLands = conspiracies.countByName(SOVREALM, false) > 0; + min -= (5 * conspiracies.countByName(ADVPROCLAMATION)); + noBasicLands = conspiracies.countByName(SOVREALM) > 0; } if (hasCommander()) { @@ -361,7 +361,7 @@ public String getDeckConformanceProblem(Deck deck) { } Integer cardCopies = canHaveSpecificNumberInDeck(simpleCard); - if (cardCopies != null && deck.getMain().countByName(cp.getKey(), true) > cardCopies) { + if (cardCopies != null && deck.getMain().countByName(cp.getKey()) > cardCopies) { return TextUtil.concatWithSpace("must not contain more than", String.valueOf(cardCopies), "copies of the card", cp.getKey()); } @@ -388,7 +388,7 @@ public String getAttractionDeckConformanceProblem(Deck deck) { 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) + if (attractionDeck.countByName(cp.getKey()) > 1) return TextUtil.concatWithSpace("contains more than 1 copy of the attraction", cp.getKey().getName()); } return null; From df40cdaea08d12f4d3654154626a81a9bd80a857 Mon Sep 17 00:00:00 2001 From: Jetz Date: Sat, 15 Jun 2024 16:44:07 -0400 Subject: [PATCH 10/20] Made DeckSection.matchingSection more suitable for general use by shifting the responsibility of recognizing potential Commanders elsewhere. --- forge-core/src/main/java/forge/deck/DeckRecognizer.java | 3 +++ forge-core/src/main/java/forge/deck/DeckSection.java | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/forge-core/src/main/java/forge/deck/DeckRecognizer.java b/forge-core/src/main/java/forge/deck/DeckRecognizer.java index 68831449471..71dbdbf5116 100644 --- a/forge-core/src/main/java/forge/deck/DeckRecognizer.java +++ b/forge-core/src/main/java/forge/deck/DeckRecognizer.java @@ -762,6 +762,9 @@ private DeckSection getTokenSection(String deckSec, DeckSection currentDeckSecti // is not supported, but other possibilities exist (e.g. Commander card in Constructed // could potentially go in Main) DeckSection matchedSection = DeckSection.matchingSection(card); + // If it's a commander candidate, put it there. + if (matchedSection == DeckSection.Main && this.isAllowed(DeckSection.Commander) && DeckSection.Commander.validate(card)) + return DeckSection.Commander; if (this.isAllowed(matchedSection)) return matchedSection; // if matched section is not allowed, try to match the card to main. diff --git a/forge-core/src/main/java/forge/deck/DeckSection.java b/forge-core/src/main/java/forge/deck/DeckSection.java index d343678ce3b..5f50c35d6ef 100644 --- a/forge-core/src/main/java/forge/deck/DeckSection.java +++ b/forge-core/src/main/java/forge/deck/DeckSection.java @@ -41,8 +41,6 @@ public static DeckSection matchingSection(PaperCard card){ return Avatar; if (DeckSection.Planes.validate(card)) return Planes; - if (DeckSection.Commander.validate(card)) - return Commander; if (DeckSection.Dungeon.validate(card)) return Dungeon; if (DeckSection.Attractions.validate(card)) From fbc475998edb30cea56bd14dd15324f78734e190 Mon Sep 17 00:00:00 2001 From: Jetz Date: Sun, 16 Jun 2024 19:54:20 -0400 Subject: [PATCH 11/20] Support for programmatically setting the advanced search filter for the mobile card manager. --- .../src/forge/itemmanager/ItemManager.java | 27 +++++ .../filters/AdvancedSearchFilter.java | 34 ++++++ .../forge/itemmanager/AdvancedSearch.java | 107 +++++++++++++++++- 3 files changed, 166 insertions(+), 2 deletions(-) diff --git a/forge-gui-mobile/src/forge/itemmanager/ItemManager.java b/forge-gui-mobile/src/forge/itemmanager/ItemManager.java index 119b34d5e64..c16a32bd67c 100644 --- a/forge-gui-mobile/src/forge/itemmanager/ItemManager.java +++ b/forge-gui-mobile/src/forge/itemmanager/ItemManager.java @@ -749,6 +749,33 @@ public void setHideFilters(boolean hideFilters0) { } } + public void applyAdvancedSearchFilter(String filterString) { + applyAdvancedSearchFilter(new String[]{filterString}, false); + } + + /** + * Programmatically method to set this ItemManager's advanced search filter value. + * Other filters will be cleared. + */ + public void applyAdvancedSearchFilter(String[] filterStrings, boolean joinAnd) { + if(advancedSearchFilter == null) { + advancedSearchFilter = createAdvancedSearchFilter(); + ItemManager.this.add(advancedSearchFilter.getWidget()); + } + lockFiltering = true; + for (final ItemFilter filter : filters) { + filter.reset(); + } + searchFilter.reset(); + advancedSearchFilter.reset(); + advancedSearchFilter.setFilterParts(filterStrings, joinAnd); + lockFiltering = false; + + applyFilters(); + advancedSearchFilter.refreshWidget(); + revalidate(); + } + //Refresh displayed items public void refresh() { updateView(true, getSelectedItems()); diff --git a/forge-gui-mobile/src/forge/itemmanager/filters/AdvancedSearchFilter.java b/forge-gui-mobile/src/forge/itemmanager/filters/AdvancedSearchFilter.java index e4870f49300..0f74898c4ba 100644 --- a/forge-gui-mobile/src/forge/itemmanager/filters/AdvancedSearchFilter.java +++ b/forge-gui-mobile/src/forge/itemmanager/filters/AdvancedSearchFilter.java @@ -7,6 +7,7 @@ import forge.Forge; import forge.assets.FSkinImage; import forge.assets.TextRenderer; +import forge.gui.GuiBase; import forge.gui.interfaces.IButton; import forge.item.InventoryItem; import forge.itemmanager.AdvancedSearch; @@ -71,6 +72,34 @@ public void reset() { editScreen = null; } + public void setFilterParts(final String[] items, boolean joinAnd) { + //This could be made more robust, processing "and"s, "or"s, and parentheses to fully configure the filter, + //but that'll have to wait until there's a use case for it. + //This could also probably be moved up to the interface and shared with the desktop version, + //but again, can't think of a use case at the moment. + this.reset(); + editScreen = new EditScreen(); + EditScreen.Filter currFilter = this.editScreen.getNewestFilter(); + for (int i = 0; i < items.length; i++) { + String filterText = items[i]; + AdvancedSearch.Filter filter = AdvancedSearch.getFilter(itemManager.getGenericType(), filterText); + if(filter == null) + continue; + currFilter.setFilter(filter); + currFilter.getBtnFilter().setText(GuiBase.getInterface().encodeSymbols(filter.toString(), false)); + if(i < items.length - 1) { + if (joinAnd) + currFilter.btnAnd.setSelected(true); + else + currFilter.btnOr.setSelected(true); + this.editScreen.addNewFilter(currFilter); + currFilter = this.editScreen.getNewestFilter(); + } + } + + onFilterChange.run(); + } + @Override protected void buildWidget(Widget widget) { label = new FiltersLabel(); @@ -203,6 +232,11 @@ protected void doLayout(float startY, float width, float height) { scroller.setBounds(0, startY, width, height - startY); } + @SuppressWarnings("unchecked") //Nothing except Filters are ever added to this FScrollPane. + private Filter getNewestFilter() { + return (Filter) scroller.getChildAt(scroller.getChildCount() - 1); + } + private void addNewFilter(Filter fromFilter) { if (scroller.getChildAt(scroller.getChildCount() - 1) == fromFilter) { Filter filter = new Filter(); diff --git a/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java b/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java index 61886e98541..c80a8ca1da4 100644 --- a/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java +++ b/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java @@ -11,11 +11,13 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import forge.card.CardEdition; import forge.card.CardRarity; @@ -1095,8 +1097,7 @@ protected boolean apply(Set inputs, List values) { private static abstract class FilterEvaluator { @SuppressWarnings("unchecked") - public final Filter createFilter(FilterOption option, FilterOperator operator) { - final List values = getValues(option, operator); + public final Filter createFilter(FilterOption option, FilterOperator operator, List values) { if (values == null || values.isEmpty()) { return null; } @@ -1127,7 +1128,26 @@ public boolean apply(T input) { return new Filter<>(option, operator, caption, predicate); } + public final Filter createFilter(FilterOption option, FilterOperator operator) { + final List values = getValues(option, operator); + return createFilter(option, operator, values); + } + + + public final Filter createFilter(FilterOption option, FilterOperator operator, String initialValueText) { + final List values; + try { + values = getValuesFromString(initialValueText, option, operator); + } + catch(Exception e) { + e.printStackTrace(); + return null; + } + return createFilter(option, operator, values); + } + protected abstract List getValues(FilterOption option, FilterOperator operator); + protected abstract List getValuesFromString(String valueText, FilterOption option, FilterOperator operator); protected abstract String getCaption(List values, FilterOption option, FilterOperator operator); protected abstract V getItemValue(T input); @@ -1147,6 +1167,11 @@ protected List getValues(FilterOption option, FilterOperator operator) return values; } + @Override + protected List getValuesFromString(String valueText, FilterOption option, FilterOperator operator) { + return getValues(option, operator); + } + @Override protected String getCaption(List values, FilterOption option, FilterOperator operator) { return String.format(operator.formatStr, option.name); @@ -1190,6 +1215,11 @@ protected List getValues(FilterOption option, FilterOperator operator) return values; } + @Override + protected List getValuesFromString(String valueText, FilterOption option, FilterOperator operator) { + return Arrays.stream(valueText.split(";")).map(String::trim).map(Integer::parseInt).collect(Collectors.toList()); + } + @Override protected String getCaption(List values, FilterOption option, FilterOperator operator) { if (operator.valueCount == FilterValueCount.TWO) { @@ -1218,6 +1248,11 @@ protected List getValues(FilterOption option, FilterOperator operator) { return values; } + @Override + protected List getValuesFromString(String valueText, FilterOption option, FilterOperator operator) { + return Lists.newArrayList(valueText); + } + @Override protected String getCaption(List values, FilterOption option, FilterOperator operator) { return String.format(operator.formatStr, option.name, values.get(0)); @@ -1247,6 +1282,20 @@ protected List getValues(FilterOption option, FilterOperator operator) { return SGuiChoose.getChoices(message, 0, max, choices, null, toLongString); } + @Override + protected List getValuesFromString(String valueText, FilterOption option, FilterOperator operator) { + String[] values = valueText.split(";"); + return choices.stream().filter((choice) -> Arrays.stream(values).anyMatch((name) -> eitherStringMatches(choice, name))).collect(Collectors.toList()); + } + + private boolean eitherStringMatches(V choice, String name) { + if(toLongString != null && name.equals(toLongString.apply(choice))) + return true; + if(toShortString != null) + return name.equals(toShortString.apply(choice)); + return name.equals(choice.toString()); + } + @Override protected String getCaption(List values, FilterOption option, FilterOperator operator) { String valuesStr; @@ -1330,6 +1379,26 @@ protected List> getValues(FilterOption option, FilterOperat return values; } + @Override + protected List> getValuesFromString(String valueText, FilterOption option, FilterOperator operator) { + int amount = -1; + String cardName; + if(operator == FilterOperator.CONTAINS_X_COPIES_OF_CARD) { + //Take the format "2 Mountain" + String[] split = valueText.split(" ", 2); + amount = Integer.parseInt(split[0]); + cardName = split[1]; + } + else + cardName = valueText; + Map map = new HashMap<>(); + map.put(cardName, amount); + + List> values = new ArrayList<>(); + values.add(map); + return values; + } + @Override protected String getCaption(List> values, FilterOption option, FilterOperator operator) { Entry entry = values.get(0).entrySet().iterator().next(); @@ -1381,6 +1450,40 @@ public static Filter getFilter(Class typ return filter; } + @SuppressWarnings("unchecked") + public static Filter getFilter(Class type, String filterText) { + String[] words = filterText.split(" ", 3); + if(words.length < 2) + { + System.out.printf("Unable to generate filter from expression '%s'%n", filterText); + return null; + } + String filterValue = words.length > 2 ? words[2] : ""; + + FilterOption option; + try { + option = FilterOption.valueOf(words[0]); + } catch (IllegalArgumentException e) { + System.out.printf("Unable to generate filter from FilterOption '%s'%n", words[0]); + return null; + } + if(option.type != type) + { + System.out.printf("Unable to generate filter from FilterOption '%s' - filter type '%s' != option type '%s' %n", words[0], type, option.type); + return null; + } + + FilterOperator operator; + try { + operator = FilterOperator.valueOf(words[1]); + } catch (IllegalArgumentException e) { + System.out.printf("Unable to generate filter from FilterOption '%s' - no matching operator '%s'%n", words[0], words[1]); + return null; + } + + return (Filter) option.evaluator.createFilter(option, operator, filterValue); + } + public static class Filter { private final FilterOption option; private final FilterOperator operator; From 88c9559eeab3b333e714b2f55add22f3c03c3ec5 Mon Sep 17 00:00:00 2001 From: Jetz Date: Sun, 16 Jun 2024 20:51:03 -0400 Subject: [PATCH 12/20] Mobile Deck Editor support for extra sections, including attractions. --- .../java/forge/card/CardRulesPredicates.java | 4 + .../src/main/java/forge/deck/DeckFormat.java | 10 +- .../src/forge/deck/FDeckEditor.java | 914 +++++++++++++----- .../src/forge/deck/FDeckImportDialog.java | 2 + .../src/forge/screens/TabPageScreen.java | 2 + forge-gui/res/languages/en-US.properties | 11 +- forge-gui/res/lists/TypeLists.txt | 1 + .../forge/itemmanager/ItemManagerConfig.java | 6 +- .../java/forge/itemmanager/SColumnUtil.java | 16 + 9 files changed, 704 insertions(+), 262 deletions(-) diff --git a/forge-core/src/main/java/forge/card/CardRulesPredicates.java b/forge-core/src/main/java/forge/card/CardRulesPredicates.java index ddd5fcf1a19..4ca7655e510 100644 --- a/forge-core/src/main/java/forge/card/CardRulesPredicates.java +++ b/forge-core/src/main/java/forge/card/CardRulesPredicates.java @@ -355,6 +355,10 @@ public boolean apply(final CardRules rules) { }; } + public static Predicate canBePartnerCommanderWith(final CardRules commander) { + return (rules) -> rules.canBePartnerCommanders(commander); + } + private static class LeafString extends PredicateString { public enum CardField { ORACLE_TEXT, NAME, SUBTYPE, JOINED_TYPE, COST diff --git a/forge-core/src/main/java/forge/deck/DeckFormat.java b/forge-core/src/main/java/forge/deck/DeckFormat.java index 6dedb34888f..6c81fac3c06 100644 --- a/forge-core/src/main/java/forge/deck/DeckFormat.java +++ b/forge-core/src/main/java/forge/deck/DeckFormat.java @@ -549,16 +549,14 @@ public boolean apply(PaperCard card) { public Predicate isLegalCardForCommanderPredicate(List commanders) { byte cmdCI = 0; - boolean hasPartner = false; for (final PaperCard p : commanders) { cmdCI |= p.getRules().getColorIdentity().getColor(); - if (p.getRules().canBePartnerCommander()) { - hasPartner = true; - } } Predicate predicate = CardRulesPredicates.hasColorIdentity(cmdCI); - if (hasPartner) { //also show available partners a commander can have a partner - predicate = Predicates.or(predicate, CardRulesPredicates.Presets.CAN_BE_PARTNER_COMMANDER); + if (commanders.size() == 1 && commanders.get(0).getRules().canBePartnerCommander()) { //also show available partners a commander can have a partner + //702.124g If a legendary card has more than one partner ability, you may choose which one to use when designating your commander, but you can’t use both. + //Notably, no partner ability or combination of partner abilities can ever let a player have more than two commanders. + predicate = Predicates.or(predicate, CardRulesPredicates.canBePartnerCommanderWith(commanders.get(0).getRules())); } return Predicates.compose(predicate, PaperCard.FN_GET_RULES); } diff --git a/forge-gui-mobile/src/forge/deck/FDeckEditor.java b/forge-gui-mobile/src/forge/deck/FDeckEditor.java index e1b900e8e8e..371191a8032 100644 --- a/forge-gui-mobile/src/forge/deck/FDeckEditor.java +++ b/forge-gui-mobile/src/forge/deck/FDeckEditor.java @@ -41,6 +41,7 @@ import java.util.*; import java.util.Map.Entry; +import java.util.stream.Collectors; public class FDeckEditor extends TabPageScreen { public static FSkinImage MAIN_DECK_ICON = Forge.hdbuttons ? FSkinImage.HDLIBRARY :FSkinImage.DECKLIST; @@ -133,6 +134,15 @@ public Deck get() { } }), null); + private static final Set LIMITED_TYPES = Collections.unmodifiableSet( + EnumSet.of(Draft, Sealed, Winston, QuestDraft) + ); + private static final Set USER_CARD_POOL_TYPES = Collections.unmodifiableSet( + EnumSet.of(Draft, Sealed, Winston, QuestDraft, Quest, QuestCommander, PlanarConquest) + ); + private static final Set COMMANDER_TYPES = Collections.unmodifiableSet( + EnumSet.of(Commander, Oathbreaker, TinyLeaders, Brawl, QuestCommander) + ); private final DeckController controller; private final Predicate cardFilter; @@ -165,6 +175,20 @@ else if (additionalFilter != null) { } return filteredPool; } + + public boolean isLimitedType() { + return LIMITED_TYPES.contains(this); + } + public boolean isCommanderType() { + return COMMANDER_TYPES.contains(this); + } + + /** + * @return true if the editor provides unlimited copies of the format's full card pool. + */ + public boolean hasInfiniteCardPool() { + return !USER_CARD_POOL_TYPES.contains(this); + } } private static DeckEditorPage[] getPages(EditorType editorType) { @@ -254,12 +278,73 @@ private static DeckEditorPage[] getPages(EditorType editorType) { } } + /** + * @return an array of optional deck sections supported by the format, but aren't usually included. + */ + public static DeckSection[] getExtraSections(EditorType editorType) { + switch (editorType) { + case Constructed: + case Commander: + return new DeckSection[]{ + DeckSection.Avatar, DeckSection.Schemes, DeckSection.Planes, DeckSection.Conspiracy, DeckSection.Attractions + }; + case Draft: + case Sealed: + return new DeckSection[]{DeckSection.Conspiracy, DeckSection.Attractions}; + } + return new DeckSection[]{DeckSection.Attractions}; + } + + private static DeckSectionPage createPageForExtraSection(DeckSection deckSection, EditorType editorType) { + switch (deckSection) { + case Avatar: + case Commander: + return new DeckSectionPage(deckSection, ItemManagerConfig.COMMANDER_SECTION); + case Schemes: + return new DeckSectionPage(deckSection, ItemManagerConfig.SCHEME_DECK_EDITOR); + case Planes: + return new DeckSectionPage(deckSection, ItemManagerConfig.PLANAR_DECK_EDITOR); + case Conspiracy: + return new DeckSectionPage(deckSection, ItemManagerConfig.CONSPIRACY_DECKS); + case Dungeon: + return new DeckSectionPage(deckSection, ItemManagerConfig.DUNGEON_DECKS); + case Attractions: + if(editorType.isLimitedType()) + return new DeckSectionPage(deckSection, ItemManagerConfig.ATTRACTION_DECK_EDITOR_LIMITED); + return new DeckSectionPage(deckSection, ItemManagerConfig.ATTRACTION_DECK_EDITOR); + default: + System.out.printf("Editor (%s) added an unsupported extra deck section - %s%n", deckSection, editorType); + return new DeckSectionPage(deckSection); + } + } + + private static String labelFromDeckSection(DeckSection deckSection) { + String label = null; + switch (deckSection) { + case Main: label = "lblMain"; break; + case Sideboard: label = "lblSide"; break; + case Commander: label = "lblCommander"; break; + case Planes: label = "lblPlanes"; break; + case Schemes: label = "lblSchemes"; break; + case Avatar: label = "lblAvatar"; break; + case Conspiracy: label = "lblConspiracies"; break; + case Attractions: label = "lblAttractions"; break; + } + String text = Localizer.getInstance().getMessage(label); + if(text == null) + return deckSection.toString(); + return text; + } + private final EditorType editorType; private Deck deck; + private final List hiddenExtraSections = new ArrayList<>(); private CatalogPage catalogPage; private DeckSectionPage mainDeckPage; private DeckSectionPage sideboardPage; private DeckSectionPage commanderPage; + private final Map pagesBySection = new EnumMap<>(DeckSection.class); + private final Set variantCardPools = new HashSet<>(); private FEventHandler saveHandler; protected final DeckHeader deckHeader = add(new DeckHeader()); @@ -280,7 +365,7 @@ public FDeckEditor(EditorType editorType0, Deck newDeck, boolean showMainDeck) { this(editorType0, "", "", newDeck, showMainDeck,null); } private FDeckEditor(EditorType editorType0, String editDeckName, String editDeckPath, Deck newDeck, boolean showMainDeck,FEventHandler backButton) { - super(backButton,getPages(editorType0)); + super(backButton, getPages(editorType0)); if (editorType0 == EditorType.QuestCommander) //fix saving quest commander editorType = EditorType.Quest; @@ -296,6 +381,7 @@ private FDeckEditor(EditorType editorType0, String editDeckName, String editDeck } else if (tabPage instanceof DeckSectionPage) { DeckSectionPage deckSectionPage = (DeckSectionPage) tabPage; + pagesBySection.put(deckSectionPage.deckSection, deckSectionPage); switch (deckSectionPage.deckSection) { case Main: case Schemes: @@ -351,6 +437,18 @@ else if (tabPage instanceof DeckSectionPage) { editorType.getController().load(editDeckPath, editDeckName); } + for(DeckSection section : getExtraSections(editorType)) { + if (deck != null && deck.has(section)) + this.showExtraSectionTab(section); + else { + this.hiddenExtraSections.add(section); + this.createExtraSectionPage(section).hideTab(); + } + } + + if(!this.getVariantCardPools().isEmpty() && editorType.hasInfiniteCardPool()) + getCatalogPage().scheduleRefresh(); + if(allowsSave()) { btnSave.setCommand(new FEventHandler() { @@ -370,8 +468,9 @@ public void handleEvent(FEvent e) { FPopupMenu menu = new FPopupMenu() { @Override protected void buildMenu() { + final Localizer localizer = Forge.getLocalizer(); if (allowsAddBasic()) - addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblAddBasicLands"), FSkinImage.LANDLOGO, new FEventHandler() { + addItem(new FMenuItem(localizer.getMessage("lblAddBasicLands"), FSkinImage.LANDLOGO, new FEventHandler() { @Override public void handleEvent(FEvent e) { CardEdition defaultLandSet; @@ -403,8 +502,28 @@ public void run(CardPool landsToAdd) { setSelectedPage(getMainDeckPage()); //select main deck page if needed so main deck is visible below dialog } })); + if (allowsAddExtraSection()) { + addItem(new FMenuItem(localizer.getMessage("lblAddDeckSection"), FSkinImage.CHAOS, new FEventHandler() { + @Override + public void handleEvent(FEvent e) { + List options = hiddenExtraSections.stream().map(FDeckEditor::labelFromDeckSection).collect(Collectors.toList()); + GuiChoose.oneOrNone(localizer.getMessage("lblAddDeckSectionSelect"), options, new Callback() { + @Override + public void run(String result) { + if(result == null || !options.contains(result)) + return; + DeckSection newSection = hiddenExtraSections.get(options.indexOf(result)); + showExtraSectionTab(newSection); + filterCatalogForExtraSection(newSection); + getCatalogPage().scheduleRefresh(); + setSelectedPage(getCatalogPage()); + } + }); + } + })); + } if (!isLimitedEditor()) { - addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblImportFromClipboard"), Forge.hdbuttons ? FSkinImage.HDIMPORT : FSkinImage.OPEN, new FEventHandler() { + addItem(new FMenuItem(localizer.getMessage("lblImportFromClipboard"), Forge.hdbuttons ? FSkinImage.HDIMPORT : FSkinImage.OPEN, new FEventHandler() { @Override public void handleEvent(FEvent e) { FDeckImportDialog dialog = new FDeckImportDialog(!deck.isEmpty(), editorType); @@ -416,17 +535,19 @@ public void run(Deck importedDeck) { lblName.setText(importedDeck.getName()); } if (dialog.createNewDeck()) { - getMainDeckPage().setCards(importedDeck.getMain()); - if (getSideboardPage() != null) - getSideboardPage().setCards(importedDeck.getOrCreate(DeckSection.Sideboard)); - if (getCommanderPage() != null) - getCommanderPage().setCards(importedDeck.getOrCreate(DeckSection.Commander)); + for(Entry section : importedDeck) + { + DeckSectionPage page = getPageForSection(section.getKey()); + if(page != null) + page.setCards(section.getValue()); + } } else { - getMainDeckPage().addCards(importedDeck.getMain()); - if (getSideboardPage() != null) - getSideboardPage().addCards(importedDeck.getOrCreate(DeckSection.Sideboard)); - if (getCommanderPage() != null) - getCommanderPage().addCards(importedDeck.getOrCreate(DeckSection.Commander)); + for(Entry section : importedDeck) + { + DeckSectionPage page = getPageForSection(section.getKey()); + if(page != null) + page.addCards(section.getValue()); + } } } }); @@ -435,11 +556,11 @@ public void run(Deck importedDeck) { } })); if(allowsSave()) - addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblSaveAs"), Forge.hdbuttons ? FSkinImage.HDSAVEAS : FSkinImage.SAVEAS, new FEventHandler() { + addItem(new FMenuItem(localizer.getMessage("lblSaveAs"), Forge.hdbuttons ? FSkinImage.HDSAVEAS : FSkinImage.SAVEAS, new FEventHandler() { @Override public void handleEvent(FEvent e) { String defaultName = editorType.getController().getNextAvailableName(); - FOptionPane.showInputDialog(Forge.getLocalizer().getMessage("lblNameNewCopyDeck"), defaultName, new Callback() { + FOptionPane.showInputDialog(localizer.getMessage("lblNameNewCopyDeck"), defaultName, new Callback() { @Override public void run(String result) { if (!StringUtils.isEmpty(result)) { @@ -451,10 +572,10 @@ public void run(String result) { })); } if (allowRename()) { - addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblRenameDeck"), Forge.hdbuttons ? FSkinImage.HDEDIT : FSkinImage.EDIT, new FEventHandler() { + addItem(new FMenuItem(localizer.getMessage("lblRenameDeck"), Forge.hdbuttons ? FSkinImage.HDEDIT : FSkinImage.EDIT, new FEventHandler() { @Override public void handleEvent(FEvent e) { - FOptionPane.showInputDialog(Forge.getLocalizer().getMessage("lblNewNameDeck"), deck.getName(), new Callback() { + FOptionPane.showInputDialog(localizer.getMessage("lblNewNameDeck"), deck.getName(), new Callback() { @Override public void run(String result) { editorType.getController().rename(result); @@ -464,12 +585,12 @@ public void run(String result) { })); } if (allowDelete()) { - addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblDeleteDeck"), Forge.hdbuttons ? FSkinImage.HDDELETE : FSkinImage.DELETE, new FEventHandler() { + addItem(new FMenuItem(localizer.getMessage("lblDeleteDeck"), Forge.hdbuttons ? FSkinImage.HDDELETE : FSkinImage.DELETE, new FEventHandler() { @Override public void handleEvent(FEvent e) { FOptionPane.showConfirmDialog( - Forge.getLocalizer().getMessage("lblConfirmDelete") + " '" + deck.getName() + "'?", - Forge.getLocalizer().getMessage("lblDeleteDeck"), Forge.getLocalizer().getMessage("lblDelete"), Forge.getLocalizer().getMessage("lblCancel"), false, new Callback() { + localizer.getMessage("lblConfirmDelete") + " '" + deck.getName() + "'?", + localizer.getMessage("lblDeleteDeck"), localizer.getMessage("lblDelete"), localizer.getMessage("lblCancel"), false, new Callback() { @Override public void run(Boolean result) { if (result) { @@ -481,7 +602,7 @@ public void run(Boolean result) { } })); } - addItem(new FMenuItem(Forge.getLocalizer().getMessage("btnCopyToClipboard"), Forge.hdbuttons ? FSkinImage.HDEXPORT : FSkinImage.BLANK, new FEventHandler() { + addItem(new FMenuItem(localizer.getMessage("btnCopyToClipboard"), Forge.hdbuttons ? FSkinImage.HDEXPORT : FSkinImage.BLANK, new FEventHandler() { @Override public void handleEvent(FEvent e) { FDeckViewer.copyDeckToClipboard(deck); @@ -533,11 +654,11 @@ protected CatalogPage getCatalogPage() { return catalogPage; } - public DeckSectionPage getMainDeckPage() { + protected DeckSectionPage getMainDeckPage() { return mainDeckPage; } - public DeckSectionPage getSideboardPage() { + protected DeckSectionPage getSideboardPage() { return sideboardPage; } @@ -545,6 +666,14 @@ protected DeckSectionPage getCommanderPage() { return commanderPage; } + protected DeckSectionPage getPageForSection(DeckSection section) { + return pagesBySection.get(section); + } + + protected Set getVariantCardPools() { + return variantCardPools; + } + public BoosterDraft getDraft() { return null; } @@ -580,6 +709,153 @@ private CardLimit getCardLimit() { } } + private int getExtraSectionMaxCopies(DeckSection section) { + switch(section) { + case Avatar: + case Commander: + case Planes: + case Dungeon: + return 1; + case Schemes: + return 2; + case Conspiracy: + return Integer.MAX_VALUE; + case Attractions: + if(isLimitedEditor()) + return Integer.MAX_VALUE; + else + return 1; + default: + return FModel.getPreferences().getPrefInt(FPref.DECK_DEFAULT_CARD_LIMIT); + } + } + + protected ItemPool getAllowedAdditions(Iterable> itemsToAdd, CardManagerPage source, CardManagerPage destination) + { + ItemPool additions = new ItemPool<>(destination.cardManager.getGenericType()); + + for (Entry itemEntry : itemsToAdd) { + PaperCard card = itemEntry.getKey(); + int numAvailable = Math.min(itemEntry.getValue(), source.cardManager.getItemCount(card)); + + int maxMovable = getMaxMovable(card, source, destination, numAvailable); + if (maxMovable <= 0) + continue; + + additions.add(card, maxMovable); + } + return additions; + } + + protected int getMaxMovable(PaperCard card, CardManagerPage source, CardManagerPage destination) { + return getMaxMovable(card, source, destination, source.cardManager.getItemCount(card)); + } + + /** + * Returns the number of copies of {@code card} that can be moved from {@code source} to {@code destination}. + *
+ * Accounts for amount available in the source page, number allowed in the deck per the format, and the card's + * own rules for number allowed in deck (e.g. Relentless Rats). + *
+ * Does not account for color identity, rules regarding designations within the command zone (e.g. whether a card + * can partner with the current commander), or the format's bans or restrictions. + */ + protected int getMaxMovable(PaperCard card, CardManagerPage source, CardManagerPage destination, int numAvailable) { + if(numAvailable <= 0) + return 0; + + if(destination instanceof CatalogPage || (isLimitedEditor() && destination == this.getSideboardPage())) { + //Removing card. Move any number. + return numAvailable; + } + + //Count copies across all deck sections. + int numInDeck = getDeck().countByName(card.getName()); + if(isLimitedEditor()) //In limited, disregard cards in sideboard. (This matters for cards like Seven Dwarves) + numInDeck -= getDeck().getOrCreate(DeckSection.Sideboard).countByName(card); + + int numAllowedInDeck = getNumAllowedInDeck(card); + + if(numAllowedInDeck == Integer.MAX_VALUE) + return numAvailable; + else if(!FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)) { + if(numAllowedInDeck == 1) //Don't prompt for quantity when editing singleton decks, even with conformity off. + return 1; + else + return numAvailable; + } + else { + //Limited number of copies. If we're adding to the deck, cap the amount accordingly. + if(source instanceof CatalogPage || (isLimitedEditor() && source == this.getSideboardPage())) + return Math.min(numAvailable, Math.max(numAllowedInDeck - numInDeck, 0)); + else + return numAvailable; + } + } + + private int getNumAllowedInDeck(PaperCard card) { + CardLimit limit = getCardLimit(); + if(DeckFormat.canHaveSpecificNumberInDeck(card) != null) + return DeckFormat.canHaveSpecificNumberInDeck(card); + else if (DeckFormat.canHaveAnyNumberOf(card)) + return Integer.MAX_VALUE; + else if (card.getRules().isVariant()) + return getExtraSectionMaxCopies(DeckSection.matchingSection(card)); + else if (limit == CardLimit.None) + return Integer.MAX_VALUE; + else if (limit == CardLimit.Singleton) + return 1; + else + return FModel.getPreferences().getPrefInt(FPref.DECK_DEFAULT_CARD_LIMIT); + } + + protected DeckSectionPage showExtraSectionTab(DeckSection section) { + this.variantCardPools.add(section); + this.hiddenExtraSections.remove(section); + DeckSectionPage page = this.getPageForSection(section); + if(page == null) + page = createExtraSectionPage(section); + page.showTab(); + return page; + } + + protected DeckSectionPage createExtraSectionPage(DeckSection section) { + DeckSectionPage page = createPageForExtraSection(section, this.editorType); + this.pagesBySection.put(section, page); + this.addTabPage(page); + page.initialize(); + return page; + } + + protected void filterCatalogForExtraSection(DeckSection section) { + CardManager cardManager = getCatalogPage().cardManager; + switch (section) { + case Avatar: + cardManager.applyAdvancedSearchFilter("CARD_TYPE CONTAINS_ALL Vanguard"); + break; + case Planes: + cardManager.applyAdvancedSearchFilter("CARD_TYPE CONTAINS_ANY Plane;Phenomenon"); + break; + case Schemes: + cardManager.applyAdvancedSearchFilter("CARD_TYPE CONTAINS_ALL Scheme"); + break; + case Conspiracy: + cardManager.applyAdvancedSearchFilter("CARD_TYPE CONTAINS_ALL Conspiracy"); + break; + case Dungeon: + cardManager.applyAdvancedSearchFilter("CARD_TYPE CONTAINS_ALL Dungeon"); + break; + case Attractions: + cardManager.applyAdvancedSearchFilter(new String[]{ + "CARD_TYPE CONTAINS_ALL Artifact", + "CARD_SUB_TYPE CONTAINS_ALL Attraction" + }, true); + break; + default: + cardManager.resetFilters(); + } + } + public void setSaveHandler(FEventHandler saveHandler0) { saveHandler = saveHandler0; } @@ -609,9 +885,9 @@ public void run(String result) { } private final static ImmutableList onCloseOptions = ImmutableList.of( - Forge.getLocalizer().getInstance().getMessage("lblSave"), - Forge.getLocalizer().getInstance().getMessage("lblDontSave"), - Forge.getLocalizer().getInstance().getMessage("lblCancel") + Localizer.getInstance().getMessage("lblSave"), + Localizer.getInstance().getMessage("lblDontSave"), + Localizer.getInstance().getMessage("lblCancel") ); @Override @@ -664,16 +940,20 @@ protected boolean allowsSave() { protected boolean allowsAddBasic() { return true; } + + /** + * @return true if the editor should show the "Add Deck Section" option in the menu. False otherwise. + */ + protected boolean allowsAddExtraSection() { + //In limited and formats with user inventories, variant cards can appear in their collection or card pool, + //so they can create the section just by adding a card to it. + return editorType.hasInfiniteCardPool() && !this.hiddenExtraSections.isEmpty(); + } protected boolean isLimitedEditor() { - switch (editorType) { - case Draft: - case Sealed: - case Winston: - case QuestDraft: - return true; - default: - return false; - } + return editorType.isLimitedType(); + } + protected boolean isCommanderEditor() { + return editorType.isCommanderType(); } protected boolean isDraftEditor() { switch (editorType) { @@ -695,7 +975,7 @@ public static boolean allowsReplacement(final EditorType editorType){ return true; default: { - if (editorType == EditorType.Draft || editorType == EditorType.Sealed || editorType == EditorType.Winston || editorType == EditorType.QuestDraft) + if (editorType.isLimitedType()) return false; else if (editorType == EditorType.PlanarConquest || editorType == EditorType.Quest || editorType == EditorType.QuestCommander) return FModel.getPreferences().getPrefBoolean(FPref.DEV_MODE_ENABLED); @@ -763,12 +1043,7 @@ protected static abstract class CardManagerPage extends DeckEditorPage { protected CardManagerPage(ItemManagerConfig config0, String caption0, FImage icon0) { super(caption0, icon0); config = config0; - cardManager.setItemActivateHandler(new FEventHandler() { - @Override - public void handleEvent(FEvent e) { - onCardActivated(cardManager.getSelectedItem()); - } - }); + cardManager.setItemActivateHandler(e -> onCardActivated(cardManager.getSelectedItem())); cardManager.setContextMenuBuilder(new ContextMenuBuilder() { @Override public void buildMenu(final FDropDownMenu menu, final PaperCard card) { @@ -817,6 +1092,8 @@ public void removeCard(PaperCard card) { removeCard(card, 1); } public void removeCard(PaperCard card, int qty) { + if (cardManager.isInfinite()) + return; cardManager.removeItem(card, qty); parentScreen.getEditorType().getController().notifyModelChanged(); updateCaption(); @@ -946,73 +1223,183 @@ public void handleEvent(FEvent e) { })); } - protected void addCommanderItems(final FDropDownMenu menu, final PaperCard card, boolean isAddMenu, boolean isAddSource) { - if (parentScreen.getCommanderPage() == null) { + protected void addMoveCardMenuItem(FDropDownMenu menu, CardManagerPage source, CardManagerPage destination, final Callback callback) { + ItemPool selectedItemPool = cardManager.getSelectedItemPool(); + if (source != this || cardManager.isInfinite()) { + //Determine how many we can actually move. + selectedItemPool = parentScreen.getAllowedAdditions(selectedItemPool, source, destination); + } + int maxMovable = selectedItemPool.isEmpty() ? 0 : Integer.MAX_VALUE; + for (Entry i : selectedItemPool) + maxMovable = Math.min(maxMovable, i.getValue()); + if (maxMovable == 0) return; + PaperCard sampleCard = cardManager.getSelectedItem(); + String labelAction, labelSection; + if(destination == null || destination instanceof CatalogPage) { + //Removing from this section, e.g. "Remove from sideboard" + labelAction = "lblRemove"; + if(source instanceof DeckSectionPage) + labelSection = getMoveLabel((DeckSectionPage) source, sampleCard, true); + else + labelSection = "lblCard"; + } + else if(destination == this && source instanceof DeckSectionPage) { + //Adding more to this section from another section, e.g. "Add from sideboard" + labelAction = "lblAdd"; + labelSection = getMoveLabel((DeckSectionPage) source, sampleCard, true); + } + else if(source instanceof DeckSectionPage && destination instanceof DeckSectionPage) { + //Moving from one named section to another, e.g. "Move to sideboard" + labelAction = "lblMove"; + labelSection = getMoveLabel((DeckSectionPage) destination, sampleCard, false); + } + else if(destination instanceof DeckSectionPage) { + //Moving from a card pool to a named section, e.g. "Add to sideboard" + DeckSectionPage deckSectionPage = (DeckSectionPage) destination; + if(deckSectionPage.deckSection == DeckSection.Commander || deckSectionPage.deckSection == DeckSection.Avatar) + labelAction = "lblSet"; + else + labelAction = "lblAdd"; + labelSection = getMoveLabel(deckSectionPage, sampleCard, false); } - boolean isLegalCommander; - String captionSuffix = Forge.getLocalizer().getMessage("lblCommander"); - switch (parentScreen.editorType) { - case Brawl: - isLegalCommander = card.getRules().canBeBrawlCommander(); - break; - case TinyLeaders: - isLegalCommander = card.getRules().canBeTinyLeadersCommander(); - break; - case Oathbreaker: - isLegalCommander = card.getRules().canBeOathbreaker(); - captionSuffix = Forge.getLocalizer().getMessage("lblOathbreaker"); - break; - case PlanarConquest: - isLegalCommander = false; //don't set commander this way in Planar Conquest - break; - default: - isLegalCommander = DeckFormat.Commander.isLegalCommander(card.getRules()); - break; - } - if (isLegalCommander && !parentScreen.getCommanderPage().cardManager.getPool().contains(card)) { - addItem(menu, "Set", "as " + captionSuffix, parentScreen.getCommanderPage().getIcon(), isAddMenu, isAddSource, new Callback() { - @Override - public void run(Integer result) { - if (result == null || result <= 0) { return; } - setCommander(card); + else { + //Moving to something that isn't a deck section or a catalog. Shouldn't ever happen and I dunno what to do if it does. + labelAction = "lblRemove"; + labelSection = "lblCard"; + } + Localizer localizer = Localizer.getInstance(); + String action = localizer.getMessage(labelAction); + String label = String.join(" ", action, localizer.getMessage(labelSection)); + String prompt = String.format("%s - %s %s", sampleCard, action, localizer.getMessage("lblHowMany")); + + FImage icon; + if(source instanceof CatalogPage && destination instanceof DeckSectionPage && ((DeckSectionPage) destination).deckSection == DeckSection.Main) + icon = Forge.hdbuttons ? FSkinImage.HDPLUS : FSkinImage.PLUS; + else if(destination == null || destination instanceof CatalogPage) + icon = Forge.hdbuttons ? FSkinImage.HDMINUS : FSkinImage.MINUS; + else + icon = destination.getIcon(); + + final int max = maxMovable; + menu.addItem(new FMenuItem(label, icon, (e) -> { + if(max < 2) + callback.run(1); + else + GuiChoose.getInteger(prompt, 1, max, 20, callback); + })); + } + + private String getMoveLabel(DeckSectionPage page, PaperCard selectedCard, boolean from) { + //This might make more sense in the DeckSection class itself, and shared with the desktop editor. + switch (page.deckSection) { + default: + case Main: return from ? "lblfromdeck" : "lbltodeck"; + case Sideboard: return from ? "lblfromsideboard" : "lbltosideboard"; + case Planes: return from ? "lblfromplanardeck" : "lbltoplanardeck"; + case Schemes: return from ? "lblfromschemedeck" : "lbltoschemedeck"; + case Conspiracy: return from ? "lblfromconspiracydeck" : "lbltoconspiracydeck"; + case Dungeon: return from ? "lblfromdungeondeck" : "lbltodungeondeck"; + case Attractions: return from ? "lblfromattractiondeck" : "lbltoattractiondeck"; + case Avatar: return "lblasavatar"; + case Commander: + if (parentScreen.editorType == EditorType.Oathbreaker) { + if(selectedCard.getRules().canBeOathbreaker()) + return "lblasoathbreaker"; + else + return "lblassignaturespell"; } - }); + else + return "lblascommander"; } - if (canHavePartnerCommander() && card.getRules().canBePartnerCommander()) { - addItem(menu, "Set", "as Partner " + captionSuffix, parentScreen.getCommanderPage().getIcon(), isAddMenu, isAddSource, new Callback() { - @Override - public void run(Integer result) { - if (result == null || result <= 0) { return; } - setPartnerCommander(card); - } - }); + } + + protected void addCommanderItems(final FDropDownMenu menu, final PaperCard card) { + if(!parentScreen.isCommanderEditor()) + return; + if(parentScreen.getMaxMovable(card, this, parentScreen.getCommanderPage()) <= 0) + return; + Localizer localizer = Forge.getLocalizer(); + String captionPrefix = localizer.getMessage("lblAddCommander"); + FImage icon = parentScreen.getCommanderPage().icon; + if (canBeCommander(card)) { + String captionSuffix; + if(parentScreen.getEditorType() == EditorType.Oathbreaker) + captionSuffix = localizer.getMessage("lblasoathbreaker"); + else + captionSuffix = localizer.getMessage("lblascommander"); + String caption = String.join(" ", captionPrefix, captionSuffix); + menu.addItem(new FMenuItem(caption, icon, e -> setCommander(card))); } - if (canHaveSignatureSpell() && card.getRules().canBeSignatureSpell()) { - addItem(menu, "Set", "as Signature Spell", FSkinImage.SORCERY, isAddMenu, isAddSource, new Callback() { - @Override - public void run(Integer result) { - if (result == null || result <= 0) { return; } - setSignatureSpell(card); - } - }); + if (canBePartnerCommander(card)) { + String caption = String.join(" ", captionPrefix, localizer.getMessage("lblaspartnercommander")); + menu.addItem(new FMenuItem(caption, icon, e -> setPartnerCommander(card))); + } + if (canBeSignatureSpell(card)) { + String caption = String.join(" ", captionPrefix, localizer.getMessage("lblassignaturespell")); + menu.addItem(new FMenuItem(caption, FSkinImage.SORCERY, e -> setSignatureSpell(card))); } } protected boolean needsCommander() { - return parentScreen.getCommanderPage() != null && parentScreen.getDeck().getCommanders().isEmpty(); + return parentScreen.isCommanderEditor() && parentScreen.getDeck().getCommanders().isEmpty(); } - protected boolean canHavePartnerCommander() { - if (parentScreen.editorType == EditorType.Oathbreaker) { - return false; //at least for now, simplify Oathbreaker by not supporting partners, since there's only one set of partner planeswalkers anyway + protected boolean canBeCommander(final PaperCard card) { + if(!parentScreen.isCommanderEditor() || parentScreen.getCommanderPage() == null) + return false; + if(parentScreen.getCommanderPage().cardManager.getPool().contains(card)) + return false; //Don't let it be the commander if it already is one. + switch (parentScreen.editorType) { + case Brawl: + return card.getRules().canBeBrawlCommander(); + case TinyLeaders: + return card.getRules().canBeTinyLeadersCommander(); + case Oathbreaker: + return card.getRules().canBeOathbreaker(); + case PlanarConquest: + return false; //don't set commander this way in Planar Conquest + default: + return DeckFormat.Commander.isLegalCommander(card.getRules()); + } + } + + protected boolean canBePartnerCommander(final PaperCard card) { + if(!parentScreen.isCommanderEditor()) + return false; + if(parentScreen.editorType == EditorType.Oathbreaker) { + //FIXME: For now, simplify Oathbreaker by not supporting partners. + //Needs support for tracking whose signature spell is whose, here and elsewhere. + return false; } - return parentScreen.getCommanderPage() != null && parentScreen.getDeck().getCommanders().size() == 1 - && parentScreen.getDeck().getCommanders().get(0).getRules().canBePartnerCommander(); + List commanders = parentScreen.getDeck().get(DeckSection.Commander).toFlatList(); + commanders.removeIf((c) -> c.getRules().canBeSignatureSpell()); + if(commanders.size() != 1) + return false; + return commanders.get(0).getRules().canBePartnerCommanders(card.getRules()); + } + + protected boolean canBeSignatureSpell(final PaperCard card) { + if(parentScreen.getEditorType() != EditorType.Oathbreaker) + return false; + PaperCard oathbreaker = parentScreen.getDeck().getOathbreaker(); + if(oathbreaker == null) + return false; + return card.getRules().canBeSignatureSpell() && card.getRules().getColorIdentity().hasNoColorsExcept(oathbreaker.getRules().getColorIdentity()); + } + + protected boolean canSideboard(final PaperCard card) { + if(parentScreen.getSideboardPage() == null) + return false; + if(parentScreen.isLimitedEditor()) + return true; + //Only allow sideboarding variant types in draft. + //I don't know if that's correct. + return DeckSection.matchingSection(card) == DeckSection.Main; } protected boolean canOnlyBePartnerCommander(final PaperCard card) { - if (parentScreen.getCommanderPage() == null) { + if (!parentScreen.isCommanderEditor()) { return false; } @@ -1024,8 +1411,17 @@ protected boolean canOnlyBePartnerCommander(final PaperCard card) { return !card.getRules().getColorIdentity().hasNoColorsExcept(cmdCI); } - protected boolean canHaveSignatureSpell() { - return parentScreen.editorType == EditorType.Oathbreaker && parentScreen.getDeck().getOathbreaker() != null; + protected boolean canBeVanguard(final PaperCard card) { + return DeckSection.matchingSection(card) == DeckSection.Avatar; + } + + protected void setVanguard(PaperCard card) { + if (!cardManager.isInfinite()) { + removeCard(card); + } + CardPool newPool = new CardPool(); + newPool.add(card); + parentScreen.getPageForSection(DeckSection.Avatar).setCards(newPool); } protected void setCommander(PaperCard card) { @@ -1077,7 +1473,7 @@ public static class CatalogPage extends CardManagerPage { private boolean initialized, needRefreshWhenShown; protected CatalogPage(ItemManagerConfig config) { - this(config, Forge.getLocalizer().getInstance().getMessage("lblCatalog"), Forge.hdbuttons ? FSkinImage.HDFOLDER : FSkinImage.FOLDER); + this(config, Localizer.getInstance().getMessage("lblCatalog"), Forge.hdbuttons ? FSkinImage.HDFOLDER : FSkinImage.FOLDER); } protected CatalogPage(ItemManagerConfig config, String caption0, FImage icon0) { super(config, caption0, icon0); @@ -1092,8 +1488,11 @@ protected void initialize() { cardManager.setCaption(getItemManagerCaption()); if (!isVisible() && (parentScreen.getEditorType() != EditorType.Quest||parentScreen.getEditorType() != EditorType.QuestCommander)) { + //delay refreshing while hidden unless for quest inventory needRefreshWhenShown = true; - return; //delay refreshing while hidden unless for quest inventory + //Throw in the all cards that might be requested by other pages. + cardManager.setPool(parentScreen.getDeck().getAllCardsInASinglePool(), true); + return; } refresh(); } @@ -1118,6 +1517,13 @@ protected String getItemManagerCaption() { } } + public void scheduleRefresh() { + if(isVisible()) + refresh(); + else + this.needRefreshWhenShown = true; + } + @Override public void setVisible(boolean visible0) { if (isVisible() == visible0) { return; } @@ -1204,31 +1610,44 @@ public void refresh() { } // fall through to below default: - if (cardManager.getWantUnique()) { - cardManager.setPool(editorType.applyCardFilter(FModel.getUniqueCardsNoAlt(), additionalFilter), true); - } else { - cardManager.setPool(editorType.applyCardFilter(FModel.getAllCardsNoAlt(), additionalFilter), true); + ItemPool cardPool = cardManager.getWantUnique() ? FModel.getUniqueCardsNoAlt() : FModel.getAllCardsNoAlt(); + //Dump all the variant cards our deck calls for into the card pool. + for(DeckSection variant : parentScreen.getVariantCardPools()) { + switch(variant) { + case Avatar: cardPool.addAll(FModel.getAvatarPool()); break; + case Conspiracy: cardPool.addAll(FModel.getConspiracyPool()); break; + case Planes: cardPool.addAll(FModel.getPlanechaseCards()); break; + case Schemes: cardPool.addAll(FModel.getArchenemyCards()); break; + case Dungeon: cardPool.addAll(FModel.getDungeonPool()); break; + case Attractions: cardPool.addAll(FModel.getAttractionPool()); break; + } } + cardManager.setPool(editorType.applyCardFilter(cardPool, additionalFilter), true); break; } } @Override protected void onCardActivated(PaperCard card) { - if (getMaxMoveQuantity(true, true) == 0) { - return; //don't add card if maximum copies of card already in deck - } + DeckSection destination = DeckSection.matchingSection(card); + final DeckSectionPage destinationPage = parentScreen.getPageForSection(destination); + if(parentScreen.getMaxMovable(card, this, destinationPage) <= 0) + return; if (needsCommander()) { setCommander(card); //handle special case of setting commander return; } + if (destination == DeckSection.Avatar) { + setVanguard(card); + return; + } if (canOnlyBePartnerCommander(card)) { return; //don't auto-change commander unexpectedly } if (!cardManager.isInfinite()) { removeCard(card); } - parentScreen.getMainDeckPage().addCard(card); + destinationPage.addCard(card); } @Override @@ -1236,35 +1655,39 @@ protected void buildMenu(final FDropDownMenu menu, final PaperCard card) { if (card == null) return; - if (!needsCommander() && !canOnlyBePartnerCommander(card)) { - addItem(menu, Forge.getLocalizer().getMessage("lblAdd"), Forge.getLocalizer().getMessage("lblTo") + " " + parentScreen.getMainDeckPage().cardManager.getCaption(), parentScreen.getMainDeckPage().getIcon(), true, true, new Callback() { + DeckSection destination = DeckSection.matchingSection(card); + final DeckSectionPage destinationPage = parentScreen.getPageForSection(destination); + + if (!needsCommander() && !canOnlyBePartnerCommander(card) && !canBeVanguard(card)) { + addMoveCardMenuItem(menu, this, destinationPage, new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } - - if (!cardManager.isInfinite()) { - removeCard(card, result); - } - parentScreen.getMainDeckPage().addCard(card, result); + + removeCard(card, result); + destinationPage.addCard(card, result); } }); - if (parentScreen.getSideboardPage() != null) { - addItem(menu, Forge.getLocalizer().getMessage("lblAdd"), Forge.getLocalizer().getMessage("lbltosideboard"), parentScreen.getSideboardPage().getIcon(), true, true, new Callback() { + if (canSideboard(card)) { + addMoveCardMenuItem(menu, this, parentScreen.getSideboardPage(), new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } - - if (!cardManager.isInfinite()) { - removeCard(card, result); - } + + removeCard(card, result); parentScreen.getSideboardPage().addCard(card, result); } }); } } + addCommanderItems(menu, card); - addCommanderItems(menu, card, true, true); + if(canBeVanguard(card)) { + Localizer localizer = Localizer.getInstance(); + String caption = String.join(" ", localizer.getMessage("lblAddCommander"), localizer.getMessage("lblasavatar")); + menu.addItem(new FMenuItem(caption, destinationPage.getIcon(), e -> setVanguard(card))); + } if (parentScreen.getEditorType() == EditorType.Constructed) { //add option to add or remove card from favorites @@ -1289,7 +1712,7 @@ public void handleEvent(FEvent e) { //if card has more than one art option, add item to change user's preferred art final List artOptions = FModel.getMagicDb().getCommonCards().getAllCardsNoAlt(card.getName()); - if (artOptions != null && artOptions.size() > 1) { + if (artOptions.size() > 1) { menu.addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblChangePreferredArt"), Forge.hdbuttons ? FSkinImage.HDPREFERENCE : FSkinImage.SETTINGS, new FEventHandler() { @Override public void handleEvent(FEvent e) { @@ -1346,38 +1769,49 @@ protected DeckSectionPage(DeckSection deckSection0, ItemManagerConfig config) { super(config, null, null); deckSection = deckSection0; + final Localizer localizer = Forge.getLocalizer(); switch (deckSection) { default: case Main: - captionPrefix = Forge.getLocalizer().getMessage("lblMain"); - cardManager.setCaption(Forge.getLocalizer().getMessage("ttMain")); + captionPrefix = localizer.getMessage("lblMain"); + cardManager.setCaption(localizer.getMessage("ttMain")); icon = MAIN_DECK_ICON; break; case Sideboard: - captionPrefix = Forge.getLocalizer().getMessage("lblSide"); - cardManager.setCaption(Forge.getLocalizer().getMessage("lblSideboard")); + captionPrefix = localizer.getMessage("lblSide"); + cardManager.setCaption(localizer.getMessage("lblSideboard")); icon = SIDEBOARD_ICON; break; case Commander: - captionPrefix = Forge.getLocalizer().getMessage("lblCommander"); - cardManager.setCaption(Forge.getLocalizer().getMessage("lblCommander")); + captionPrefix = localizer.getMessage("lblCommander"); + cardManager.setCaption(localizer.getMessage("lblCommander")); icon = FSkinImage.COMMANDER; break; case Avatar: - captionPrefix = Forge.getLocalizer().getMessage("lblAvatar"); - cardManager.setCaption(Forge.getLocalizer().getMessage("lblAvatar")); + captionPrefix = localizer.getMessage("lblAvatar"); + cardManager.setCaption(localizer.getMessage("lblAvatar")); icon = new FTextureRegionImage(FSkin.getAvatars().get(0)); break; + case Conspiracy: + captionPrefix = localizer.getMessage("lblConspiracies"); + cardManager.setCaption(localizer.getMessage("lblConspiracies")); + icon = FSkinImage.UNKNOWN; //TODO: This and the other extra sections definitely need better icons. + break; case Planes: - captionPrefix = Forge.getLocalizer().getMessage("lblPlanes"); - cardManager.setCaption(Forge.getLocalizer().getMessage("lblPlanes")); + captionPrefix = localizer.getMessage("lblPlanes"); + cardManager.setCaption(localizer.getMessage("lblPlanes")); icon = FSkinImage.CHAOS; break; case Schemes: - captionPrefix = Forge.getLocalizer().getMessage("lblSchemes"); - cardManager.setCaption(Forge.getLocalizer().getMessage("lblSchemes")); + captionPrefix = localizer.getMessage("lblSchemes"); + cardManager.setCaption(localizer.getMessage("lblSchemes")); icon = FSkinImage.POISON; break; + case Attractions: + captionPrefix = localizer.getMessage("lblAttractions"); + cardManager.setCaption(localizer.getMessage("lblAttractions")); + icon = FSkinImage.TICKET; + break; } } protected DeckSectionPage(DeckSection deckSection0, ItemManagerConfig config, String caption0, FImage icon0) { @@ -1404,12 +1838,33 @@ protected void updateCaption() { } } + @Override + public void addCard(PaperCard card, int qty) { + super.addCard(card, qty); + if(parentScreen.hiddenExtraSections.contains(this.deckSection)) + parentScreen.showExtraSectionTab(this.deckSection); + } + @Override + public void addCards(Iterable> cards) { + super.addCards(cards); + if(parentScreen.hiddenExtraSections.contains(this.deckSection)) + parentScreen.showExtraSectionTab(this.deckSection); + } + + @Override + public void setCards(CardPool cards) { + super.setCards(cards); + if(parentScreen.hiddenExtraSections.contains(this.deckSection) && !cards.isEmpty()) + parentScreen.showExtraSectionTab(this.deckSection); + } + @Override protected void onCardActivated(PaperCard card) { switch (deckSection) { case Main: case Planes: case Schemes: + case Attractions: removeCard(card); switch (parentScreen.getEditorType()) { case Draft: @@ -1435,38 +1890,39 @@ protected void onCardActivated(PaperCard card) { @Override protected void buildMenu(final FDropDownMenu menu, final PaperCard card) { + FSkinImage iconReplaceCard = Forge.hdbuttons ? FSkinImage.HDCHOICE : FSkinImage.DECKLIST; + final Localizer localizer = Forge.getLocalizer(); + String lblReplaceCard = localizer.getMessage("lblReplaceCard"); + + CardManagerPage cardSourceSection; + DeckSection destination = DeckSection.matchingSection(card); + final DeckSectionPage destinationPage = parentScreen.getPageForSection(destination); switch (deckSection) { default: case Main: - addItem(menu, Forge.getLocalizer().getMessage("lblAdd"), null, Forge.hdbuttons ? FSkinImage.HDPLUS : FSkinImage.PLUS, true, false, new Callback() { + cardSourceSection = parentScreen.isLimitedEditor() ? parentScreen.getSideboardPage() : parentScreen.getCatalogPage(); + addMoveCardMenuItem(menu, cardSourceSection, this, new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } - if (parentScreen.isLimitedEditor()) { //ensure card removed from sideboard before adding to main - parentScreen.getSideboardPage().removeCard(card, result); - } - else if (parentScreen.getEditorType() == EditorType.Quest || parentScreen.getEditorType() == EditorType.QuestCommander) { - parentScreen.getCatalogPage().removeCard(card, result); - } + cardSourceSection.removeCard(card, result); //ensure card removed from sideboard before adding to main addCard(card, result); } }); if (!parentScreen.isLimitedEditor()) { - addItem(menu, Forge.getLocalizer().getMessage("lblRemove"), null, Forge.hdbuttons ? FSkinImage.HDMINUS : FSkinImage.MINUS, false, false, new Callback() { + addMoveCardMenuItem(menu, this, cardSourceSection, new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } removeCard(card, result); - if (parentScreen.getCatalogPage() != null) { - parentScreen.getCatalogPage().addCard(card, result); - } + cardSourceSection.addCard(card, result); } }); } if (parentScreen.getSideboardPage() != null) { - addItem(menu, Forge.getLocalizer().getMessage("lblMove"), Forge.getLocalizer().getMessage("lbltosideboard"), parentScreen.getSideboardPage().getIcon(), false, false, new Callback() { + addMoveCardMenuItem(menu, this, parentScreen.getSideboardPage(), new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } @@ -1478,106 +1934,54 @@ public void run(Integer result) { } if (parentScreen.isAllowedReplacement()) { final List cardOptions = FModel.getMagicDb().getCommonCards().getAllCardsNoAlt(card.getName()); - if (cardOptions != null && cardOptions.size() > 1) { - menu.addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblReplaceCard"), Forge.hdbuttons ? FSkinImage.HDCHOICE : FSkinImage.DECKLIST, new FEventHandler() { - @Override - public void handleEvent(FEvent e) { - //sort options so current option is on top and selected by default - List sortedOptions = new ArrayList<>(); - sortedOptions.add(card); - for (PaperCard option : cardOptions) { - if (option != card) { - sortedOptions.add(option); - } - } - GuiChoose.oneOrNone(Forge.getLocalizer().getMessage("lblSelectReplacementCard") + " " + card.getName(), sortedOptions, new Callback() { - @Override - public void run(PaperCard result) { - if (result != null) { - if (result != card) { - addCard(result); - removeCard(card); - } - } - } - }); - } - })); + if (cardOptions.size() > 1) { + menu.addItem(new FMenuItem(lblReplaceCard, iconReplaceCard, e -> handleReplaceCard(card, cardOptions))); } } - addCommanderItems(menu, card, false, false); + addCommanderItems(menu, card); break; case Sideboard: - addItem(menu, Forge.getLocalizer().getMessage("lblAdd"), null, Forge.hdbuttons ? FSkinImage.HDPLUS : FSkinImage.PLUS, true, false, new Callback() { + cardSourceSection = parentScreen.isLimitedEditor() ? parentScreen.getMainDeckPage() : parentScreen.getCatalogPage(); + addMoveCardMenuItem(menu, cardSourceSection, this, new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } - if (parentScreen.isLimitedEditor()) { //ensure card removed from main deck before adding to sideboard - parentScreen.getMainDeckPage().removeCard(card, result); - } - else if (parentScreen.getEditorType() == EditorType.Quest || parentScreen.getEditorType() == EditorType.QuestCommander) { - parentScreen.getCatalogPage().removeCard(card, result); - } + cardSourceSection.removeCard(card, result); //ensure card removed from main deck before adding to sideboard addCard(card, result); } }); if (!parentScreen.isLimitedEditor()) { - addItem(menu, Forge.getLocalizer().getMessage("lblRemove"), null, Forge.hdbuttons ? FSkinImage.HDMINUS : FSkinImage.MINUS, false, false, new Callback() { + addMoveCardMenuItem(menu, this, cardSourceSection, new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } removeCard(card, result); - if (parentScreen.getCatalogPage() != null) { - parentScreen.getCatalogPage().addCard(card, result); - } + cardSourceSection.addCard(card, result); } }); } - addItem(menu, Forge.getLocalizer().getMessage("lblMove"), Forge.getLocalizer().getMessage("lblToMainDeck"), parentScreen.getMainDeckPage().getIcon(), false, false, new Callback() { + addMoveCardMenuItem(menu, this, destinationPage, new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } removeCard(card, result); - parentScreen.getMainDeckPage().addCard(card, result); + destinationPage.addCard(card, result); } }); if (parentScreen.isAllowedReplacement()) { final List cardOptions = FModel.getMagicDb().getCommonCards().getAllCardsNoAlt(card.getName()); - if (cardOptions != null && cardOptions.size() > 1) { - menu.addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblReplaceCard"), Forge.hdbuttons ? FSkinImage.HDCHOICE : FSkinImage.DECKLIST, new FEventHandler() { - @Override - public void handleEvent(FEvent e) { - //sort options so current option is on top and selected by default - List sortedOptions = new ArrayList<>(); - sortedOptions.add(card); - for (PaperCard option : cardOptions) { - if (option != card) { - sortedOptions.add(option); - } - } - GuiChoose.oneOrNone(Forge.getLocalizer().getMessage("lblSelectReplacementCard") + " " + card.getName(), sortedOptions, new Callback() { - @Override - public void run(PaperCard result) { - if (result != null) { - if (result != card) { - addCard(result); - removeCard(card); - } - } - } - }); - } - })); + if (cardOptions.size() > 1) { + menu.addItem(new FMenuItem(lblReplaceCard, iconReplaceCard, e -> handleReplaceCard(card, cardOptions))); } } - addCommanderItems(menu, card, false, false); + addCommanderItems(menu, card); break; case Commander: if (parentScreen.editorType != EditorType.PlanarConquest || isPartnerCommander(card)) { - addItem(menu, Forge.getLocalizer().getMessage("lblRemove"), null, Forge.hdbuttons ? FSkinImage.HDMINUS : FSkinImage.MINUS, false, false, new Callback() { + addMoveCardMenuItem(menu, this, parentScreen.getCatalogPage(), new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { @@ -1592,36 +1996,13 @@ public void run(Integer result) { } if (parentScreen.isAllowedReplacement()) { final List cardOptions = FModel.getMagicDb().getCommonCards().getAllCardsNoAlt(card.getName()); - if (cardOptions != null && cardOptions.size() > 1) { - menu.addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblReplaceCard"), Forge.hdbuttons ? FSkinImage.HDCHOICE : FSkinImage.DECKLIST, new FEventHandler() { - @Override - public void handleEvent(FEvent e) { - //sort options so current option is on top and selected by default - List sortedOptions = new ArrayList<>(); - sortedOptions.add(card); - for (PaperCard option : cardOptions) { - if (option != card) { - sortedOptions.add(option); - } - } - GuiChoose.oneOrNone(Forge.getLocalizer().getMessage("lblSelectReplacementCard") + " " + card.getName(), sortedOptions, new Callback() { - @Override - public void run(PaperCard result) { - if (result != null) { - if (result != card) { - addCard(result); - removeCard(card); - } - } - } - }); - } - })); + if (cardOptions.size() > 1) { + menu.addItem(new FMenuItem(lblReplaceCard, iconReplaceCard, e -> handleReplaceCard(card, cardOptions))); } } break; case Avatar: - addItem(menu, Forge.getLocalizer().getMessage("lblRemove"), null, Forge.hdbuttons ? FSkinImage.HDMINUS : FSkinImage.MINUS, false, false, new Callback() { + addMoveCardMenuItem(menu, this, parentScreen.getCatalogPage(), new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } @@ -1631,7 +2012,7 @@ public void run(Integer result) { }); break; case Schemes: - addItem(menu, Forge.getLocalizer().getMessage("lblAdd"), null, Forge.hdbuttons ? FSkinImage.HDPLUS : FSkinImage.PLUS, true, false, new Callback() { + addMoveCardMenuItem(menu, parentScreen.getCatalogPage(), this, new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } @@ -1639,7 +2020,7 @@ public void run(Integer result) { addCard(card, result); } }); - addItem(menu, Forge.getLocalizer().getMessage("lblRemove"), null, Forge.hdbuttons ? FSkinImage.HDMINUS : FSkinImage.MINUS, false, false, new Callback() { + addMoveCardMenuItem(menu, this, parentScreen.getCatalogPage(), new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } @@ -1649,15 +2030,8 @@ public void run(Integer result) { }); break; case Planes: - addItem(menu, Forge.getLocalizer().getMessage("lblAdd"), null, Forge.hdbuttons ? FSkinImage.HDPLUS : FSkinImage.PLUS, true, false, new Callback() { - @Override - public void run(Integer result) { - if (result == null || result <= 0) { return; } - - addCard(card, result); - } - }); - addItem(menu, Forge.getLocalizer().getMessage("lblRemove"), null, Forge.hdbuttons ? FSkinImage.HDMINUS : FSkinImage.MINUS, false, false, new Callback() { + case Attractions: + addMoveCardMenuItem(menu, this, parentScreen.getCatalogPage(), new Callback() { @Override public void run(Integer result) { if (result == null || result <= 0) { return; } @@ -1665,12 +2039,41 @@ public void run(Integer result) { removeCard(card, result); } }); + if (parentScreen.isAllowedReplacement()) { + final List cardOptions = FModel.getMagicDb().getCommonCards().getAllCardsNoAlt(card.getName()); + if (cardOptions.size() > 1) { + menu.addItem(new FMenuItem(lblReplaceCard, iconReplaceCard, e -> handleReplaceCard(card, cardOptions))); + } + } break; } } + private void handleReplaceCard(PaperCard card, List cardOptions) { + //sort options so current option is on top and selected by default + List sortedOptions = new ArrayList<>(); + sortedOptions.add(card); + for (PaperCard option : cardOptions) { + if (option != card) { + sortedOptions.add(option); + } + } + String prompt = Forge.getLocalizer().getMessage("lblSelectReplacementCard") + " " + card.getName(); + GuiChoose.oneOrNone(prompt, sortedOptions, new Callback() { + @Override + public void run(PaperCard result) { + if (result != null) { + if (result != card) { + addCard(result); + removeCard(card); + } + } + } + }); + } + private boolean isPartnerCommander(final PaperCard card) { - if (parentScreen.getCommanderPage() == null || parentScreen.getDeck().getCommanders().isEmpty()) { + if (!parentScreen.isCommanderEditor() || parentScreen.getDeck().getCommanders().isEmpty()) { return false; } @@ -1681,7 +2084,7 @@ private boolean isPartnerCommander(final PaperCard card) { private static class DraftPackPage extends CatalogPage { protected DraftPackPage() { - super(ItemManagerConfig.DRAFT_PACK, Forge.getLocalizer().getInstance().getMessage("lblPackN", String.valueOf(1)), FSkinImage.PACK); + super(ItemManagerConfig.DRAFT_PACK, Localizer.getInstance().getMessage("lblPackN", String.valueOf(1)), FSkinImage.PACK); } @Override @@ -1719,14 +2122,19 @@ private void afterCardPicked(PaperCard card) { @Override protected void buildMenu(final FDropDownMenu menu, final PaperCard card) { - addItem(menu, Forge.getLocalizer().getMessage("lblAdd"), Forge.getLocalizer().getMessage("lblToMainDeck"), parentScreen.getMainDeckPage().getIcon(), true, true, new Callback() { + DeckSection destination = DeckSection.matchingSection(card); + final DeckSectionPage destinationPage = parentScreen.getPageForSection(destination); + addMoveCardMenuItem(menu, this, destinationPage, new Callback() { @Override public void run(Integer result) { //ignore quantity - parentScreen.getMainDeckPage().addCard(card); + DeckSectionPage destinationPage = parentScreen.getPageForSection(destination); + if(destinationPage == null) + destinationPage = parentScreen.showExtraSectionTab(destination); + destinationPage.addCard(card); afterCardPicked(card); } }); - addItem(menu, Forge.getLocalizer().getMessage("lblAdd"), Forge.getLocalizer().getMessage("lbltosideboard"), parentScreen.getSideboardPage().getIcon(), true, true, new Callback() { + addMoveCardMenuItem(menu, this, parentScreen.getSideboardPage(), new Callback() { @Override public void run(Integer result) { //ignore quantity parentScreen.getSideboardPage().addCard(card); diff --git a/forge-gui-mobile/src/forge/deck/FDeckImportDialog.java b/forge-gui-mobile/src/forge/deck/FDeckImportDialog.java index a4ad107b3fc..f3c93950bd2 100644 --- a/forge-gui-mobile/src/forge/deck/FDeckImportDialog.java +++ b/forge-gui-mobile/src/forge/deck/FDeckImportDialog.java @@ -21,6 +21,7 @@ import java.util.List; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import forge.Forge; import forge.Graphics; @@ -81,6 +82,7 @@ public FDeckImportDialog(final boolean replacingDeck, final FDeckEditor.EditorTy supportedSections.add(DeckSection.Sideboard); if (editorType != FDeckEditor.EditorType.Constructed) supportedSections.add(DeckSection.Commander); + supportedSections.addAll(Lists.newArrayList(FDeckEditor.getExtraSections(editorType))); controller.setAllowedSections(supportedSections); } diff --git a/forge-gui-mobile/src/forge/screens/TabPageScreen.java b/forge-gui-mobile/src/forge/screens/TabPageScreen.java index 0be8ee16028..9150690df9d 100644 --- a/forge-gui-mobile/src/forge/screens/TabPageScreen.java +++ b/forge-gui-mobile/src/forge/screens/TabPageScreen.java @@ -85,6 +85,7 @@ public void addTabPage(TabPage tabPage) { tabPage.parentScreen = (T) this; add(tabPage); tabPage.setVisible(false); + this.revalidate(); } public TabPage getSelectedPage() { @@ -227,6 +228,7 @@ public TabHeader(TabPage[] tabPages, FEventHandler backButton) { public void addTab(TabPage tabPage) { this.tabPages.add(tabPage); this.scroller.add(tabPage.tab); + this.scroller.revalidate(); } protected boolean showBackButtonInLandscapeMode() { diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index f8be5d066d4..4be3af5713f 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -894,14 +894,16 @@ lblColumns=Columns lblPiles=Piles: lblGroups=Groups: lblImageView=Image View -#CEditorVariant.java, CEditorConstructed.java +#CEditorVariant.java, CEditorConstructed.java, FDeckEditor.java lblCatalog=Catalog lblAdd=Add +lblAddCommander=Set lbltodeck=to deck lblfromdeck=from deck lbltosideboard=to sideboard lblfromsideboard=from sideboard lblascommander=as commander +lblaspartnercommander=as partner commander lblasoathbreaker=as oathbreaker lblassignaturespell=as signature spell lblasavatar=as avatar @@ -909,7 +911,7 @@ lblfromschemedeck=from scheme deck lblfromplanardeck=from planar deck lblfromconspiracydeck=from conspiracy deck lblfromdungeondeck=from dungeon deck -lblfromattractiondeck=from attraction deck +lblfromattractiondeck=from attraction deck lbltoschemedeck=to scheme deck lbltoplanardeck=to planar deck lbltoconspiracydeck=to conspiracy deck @@ -1123,17 +1125,22 @@ lblChangePreferredArt=Change Preferred Art lblSelectPreferredArt=Select preferred art for lblReplaceCard=Replace Card Variant lblSelectReplacementCard=Select Replacement Card Variant for +lblAddDeckSection=Add Variant Section... +lblAddDeckSectionSelect=Select Section to Add lblTo=to +lblFrom=from lblAvatar=Avatar lblCards=Cards lblPlanes=Planes lblSchemes=Schemes +lblAttractions=Attractions lblToMainDeck=to Main Deck lblHowMany=how many? lblInventory=Inventory lblCollection=Collection lblCommanders=Commanders lblOathbreakers=Oathbreakers +lblConspiracies=Conspiracies lblSave=Save lblDontSave=Don''t Save lblPackN=Pack {0} diff --git a/forge-gui/res/lists/TypeLists.txt b/forge-gui/res/lists/TypeLists.txt index 1f9993666f3..40d50f30d03 100644 --- a/forge-gui/res/lists/TypeLists.txt +++ b/forge-gui/res/lists/TypeLists.txt @@ -336,6 +336,7 @@ Saga Shrine Shard [ArtifactTypes] +Attraction Blood Bobblehead:Bobbleheads Clue:Clues diff --git a/forge-gui/src/main/java/forge/itemmanager/ItemManagerConfig.java b/forge-gui/src/main/java/forge/itemmanager/ItemManagerConfig.java index 6093be9eee1..79c89f8de88 100644 --- a/forge-gui/src/main/java/forge/itemmanager/ItemManagerConfig.java +++ b/forge-gui/src/main/java/forge/itemmanager/ItemManagerConfig.java @@ -66,7 +66,11 @@ 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, + ATTRACTION_POOL(SColumnUtil.getAttractionPoolDefaultColumns(), false, false, true, + null, null, 4, 0), + ATTRACTION_DECK_EDITOR(SColumnUtil.getCatalogDefaultColumns(true), false, false, true, + null, null, 4, 0), + ATTRACTION_DECK_EDITOR_LIMITED(SColumnUtil.getCatalogDefaultColumns(false), false, false, true, null, null, 4, 0), COMMANDER_POOL(SColumnUtil.getCatalogDefaultColumns(true), true, false, false, null, null, 4, 0), diff --git a/forge-gui/src/main/java/forge/itemmanager/SColumnUtil.java b/forge-gui/src/main/java/forge/itemmanager/SColumnUtil.java index df138db05b5..7a8bf4efbc4 100644 --- a/forge-gui/src/main/java/forge/itemmanager/SColumnUtil.java +++ b/forge-gui/src/main/java/forge/itemmanager/SColumnUtil.java @@ -143,6 +143,22 @@ public static Map getSpecialCardPoolDefaultColumns( return columns; } + public static Map getAttractionPoolDefaultColumns() { + //Similar to special card pool, but show the collector number and hide the type. + List colDefs = new ArrayList<>(); + colDefs.add(ColumnDef.FAVORITE); + colDefs.add(ColumnDef.NAME); + colDefs.add(ColumnDef.RARITY); + colDefs.add(ColumnDef.SET); + colDefs.add(ColumnDef.COLLECTOR_ORDER); + + Map columns = getColumns(colDefs); + columns.get(ColumnDef.FAVORITE).setSortPriority(1); + columns.get(ColumnDef.NAME).setSortPriority(2); + columns.get(ColumnDef.COLLECTOR_ORDER).setSortPriority(3); + return columns; + } + public static Map getSpellShopDefaultColumns() { Map columns = getCardColumns(ColumnDef.QUANTITY, false, true, true, false, false); columns.get(ColumnDef.OWNED).setSortPriority(1); From bf1f1ecb616c2e630801758c56fc0cf9d58b0b68 Mon Sep 17 00:00:00 2001 From: Jetz Date: Mon, 17 Jun 2024 19:03:23 -0400 Subject: [PATCH 13/20] Pick-a-Beeble and The Most Dangerous Gamer. Prize mechanic. --- .../main/java/forge/game/ability/ApiType.java | 1 + .../ability/effects/ClaimThePrizeEffect.java | 38 +++++++++++++++++++ .../java/forge/game/card/CardFactoryUtil.java | 13 ++++++- .../forge/game/trigger/TriggerClaimPrize.java | 36 ++++++++++++++++++ .../java/forge/game/trigger/TriggerType.java | 1 + .../game/trigger/TriggerVisitAttraction.java | 1 - forge-gui/res/cardsfolder/p/pick_a_beeble.txt | 19 ++++++++++ .../t/the_most_dangerous_gamer.txt | 13 +++++++ 8 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/ability/effects/ClaimThePrizeEffect.java create mode 100644 forge-game/src/main/java/forge/game/trigger/TriggerClaimPrize.java create mode 100644 forge-gui/res/cardsfolder/p/pick_a_beeble.txt create mode 100644 forge-gui/res/cardsfolder/t/the_most_dangerous_gamer.txt 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 2e71f1c1108..4f0c3a25efc 100644 --- a/forge-game/src/main/java/forge/game/ability/ApiType.java +++ b/forge-game/src/main/java/forge/game/ability/ApiType.java @@ -50,6 +50,7 @@ public enum ApiType { ChooseSector (ChooseSectorEffect.class), ChooseSource (ChooseSourceEffect.class), ChooseType (ChooseTypeEffect.class), + ClaimThePrize (ClaimThePrizeEffect.class), Clash (ClashEffect.class), ClassLevelUp (ClassLevelUpEffect.class), Cleanup (CleanUpEffect.class), diff --git a/forge-game/src/main/java/forge/game/ability/effects/ClaimThePrizeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ClaimThePrizeEffect.java new file mode 100644 index 00000000000..a97ffccc6bf --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/ClaimThePrizeEffect.java @@ -0,0 +1,38 @@ +package forge.game.ability.effects; + +import forge.game.Game; +import forge.game.ability.AbilityKey; +import forge.game.ability.AbilityUtils; +import forge.game.ability.SpellAbilityEffect; +import forge.game.card.Card; +import forge.game.card.CardCollection; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.trigger.TriggerType; +import forge.util.Lang; + +import java.util.Map; + +public class ClaimThePrizeEffect extends SpellAbilityEffect { + + @Override + public void resolve(SpellAbility sa) { + final Card host = sa.getHostCard(); + final Player activator = sa.getActivatingPlayer(); + final Game game = activator.getGame(); + final CardCollection attractions = AbilityUtils.getDefinedCards(host, sa.getParamOrDefault("Defined", "Self"), sa); + + for(Card c : attractions) { + final Map runParams = AbilityKey.mapFromPlayer(activator); + runParams.put(AbilityKey.Card, c); + game.getTriggerHandler().runTrigger(TriggerType.ClaimPrize, runParams, false); + } + } + + @Override + protected String getStackDescription(SpellAbility sa) { + final Card host = sa.getHostCard(); + final CardCollection attractions = AbilityUtils.getDefinedCards(host, sa.getParamOrDefault("Defined", "Self"), sa); + return String.format("Claim the Prize from %s!", Lang.joinHomogenous(attractions)); + } +} 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 4f4dadb8b57..7b2857499ae 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -1967,7 +1967,6 @@ public static void addTriggerAbility(final KeywordInterface inst, final Card car 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(); @@ -1979,6 +1978,18 @@ public static void addTriggerAbility(final KeywordInterface inst, final Card car t.setOverridingAbility(sa); inst.addTrigger(t); + } else if (keyword.startsWith("Prize")) { + final String[] k = keyword.split(":"); + + SpellAbility sa = AbilityFactory.getAbility(card, k[1]); //Is this the right thing? + String descStr = "Prize — " + sa.getDescription(); + + final String trigStr = "Mode$ ClaimPrize | Static$ True | 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/trigger/TriggerClaimPrize.java b/forge-game/src/main/java/forge/game/trigger/TriggerClaimPrize.java new file mode 100644 index 00000000000..44331b0ca60 --- /dev/null +++ b/forge-game/src/main/java/forge/game/trigger/TriggerClaimPrize.java @@ -0,0 +1,36 @@ +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 TriggerClaimPrize extends Trigger{ + public TriggerClaimPrize(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) { + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player, AbilityKey.Card); + } + + @Override + public String getImportantStackObjects(SpellAbility sa) { + return Localizer.getInstance().getMessage("lblPlayer") + ": " + + sa.getTriggeringObject(AbilityKey.Player); + } +} 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 b3e80b7493e..c1498810b56 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerType.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerType.java @@ -43,6 +43,7 @@ public enum TriggerType { ChangesZone(TriggerChangesZone.class), ChangesZoneAll(TriggerChangesZoneAll.class), ChaosEnsues(TriggerChaosEnsues.class), + ClaimPrize(TriggerClaimPrize.class), Clashed(TriggerClashed.class), ClassLevelGained(TriggerClassLevelGained.class), CommitCrime(TriggerCommitCrime.class), diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerVisitAttraction.java b/forge-game/src/main/java/forge/game/trigger/TriggerVisitAttraction.java index c22fad87d03..754c67e3e66 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerVisitAttraction.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerVisitAttraction.java @@ -33,7 +33,6 @@ public void setTriggeringObjects(SpellAbility sa, Map runPar @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-gui/res/cardsfolder/p/pick_a_beeble.txt b/forge-gui/res/cardsfolder/p/pick_a_beeble.txt new file mode 100644 index 00000000000..9b0d37de613 --- /dev/null +++ b/forge-gui/res/cardsfolder/p/pick_a_beeble.txt @@ -0,0 +1,19 @@ +Name:Pick-a-Beeble +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:TrigRoll +K:Prize:TrigPrize +SVar:TrigRoll:DB$ RollDice | ResultSVar$ Result | Sides$ 6 | SubAbility$ DBCounters | SpellDescription$ Roll a six-sided die. Put a number of luck counters on CARDNAME equal to the result and create a Treasure token. Then if there are six or more luck counters on CARDNAME, claim the prize! +SVar:DBCounters:DB$ PutCounter | Defined$ Self | CounterType$ LUCK | CounterNum$ Result | SubAbility$ DBTreasure +SVar:DBTreasure:DB$ Token | TokenAmount$ 1 | TokenScript$ c_a_treasure_sac | SubAbility$ DBClaim +SVar:DBClaim:DB$ ClaimThePrize | ConditionDefined$ Self | ConditionPresent$ Card.Self+counters_GE6_LUCK +SVar:TrigPrize:DB$ Token | TokenAmount$ 2 | TokenScript$ c_a_treasure_sac | SubAbility$ DBSack | SpellDescription$ Create two Treasure tokens, then sacrifice CARDNAME and open an Attraction. +SVar:DBSack:DB$ Sacrifice | SacValid$ Self | SubAbility$ DBOpen +SVar:DBOpen:DB$ OpenAttraction +Oracle:Visit — Roll a six-sided die. Put a number of luck counters on Pick-a-Beeble equal to the result and create a Treasure token. Then if there are six or more luck counters on Pick-a-Beeble, claim the prize!\nPrize — Create two Treasure tokens, then sacrifice Pick-a-Beeble and open an Attraction. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/t/the_most_dangerous_gamer.txt b/forge-gui/res/cardsfolder/t/the_most_dangerous_gamer.txt new file mode 100644 index 00000000000..33c6c5d65f7 --- /dev/null +++ b/forge-gui/res/cardsfolder/t/the_most_dangerous_gamer.txt @@ -0,0 +1,13 @@ +Name:The Most Dangerous Gamer +ManaCost:2 B G +Types:Legendary Creature Human Gamer Guest +PT:2/2 +K:Deathtouch +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ DBOpen | TriggerDescription$ Whenever CARDNAME enters the battlefield or attacks, open an Attraction. +T:Mode$ Attacks | ValidCard$ Card.Self | Secondary$ True | Execute$ DBOpen | TriggerDescription$ Whenever CARDNAME enters the battlefield or attacks, open an Attraction. +SVar:DBOpen:DB$ OpenAttraction +T:Mode$ ChangesZone | Origin$ AttractionDeck | Destination$ Battlefield | ValidCard$ Attraction.YouCtrl | TriggerZones$ Battlefield | Execute$ DBCounter | TriggerDescription$ Whenever you open an Attraction, put a +1/+1 counter on CARDNAME. +SVar:DBCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1 +T:Mode$ ClaimPrize | ValidCard$ Attraction.YouCtrl | TriggerZones$ Battlefield | Execute$ DBDestroy | TriggerDescription$ Whenever you claim the prize of an Attraction, destroy target permanent. +SVar:DBDestroy:DB$ Destroy | ValidTgts$ Permanent | TgtPrompt$ Select target permanent. +Oracle:Deathtouch\nWhenever The Most Dangerous Gamer enters the battlefield or attacks, open an Attraction.\nWhenever you open an Attraction, put a +1/+1 counter on The Most Dangerous Gamer.\nWhenever you claim the prize of an Attraction, destroy target permanent. From 657ebfda259374953609c17c9a96d4e3ae7b51ca Mon Sep 17 00:00:00 2001 From: Jetz Date: Wed, 19 Jun 2024 19:03:29 -0400 Subject: [PATCH 14/20] 8 more attractions. --- forge-gui/res/cardsfolder/b/bounce_chamber.txt | 11 +++++++++++ forge-gui/res/cardsfolder/f/foam_weapons_kiosk.txt | 12 ++++++++++++ forge-gui/res/cardsfolder/h/haunted_house.txt | 9 +++++++++ forge-gui/res/cardsfolder/k/kiddie_coaster.txt | 12 ++++++++++++ forge-gui/res/cardsfolder/m/merry_go_round.txt | 8 ++++++++ forge-gui/res/cardsfolder/r/roller_coaster.txt | 10 ++++++++++ forge-gui/res/cardsfolder/s/spinny_ride.txt | 12 ++++++++++++ forge-gui/res/cardsfolder/t/trash_bin.txt | 12 ++++++++++++ 8 files changed, 86 insertions(+) create mode 100644 forge-gui/res/cardsfolder/b/bounce_chamber.txt create mode 100644 forge-gui/res/cardsfolder/f/foam_weapons_kiosk.txt create mode 100644 forge-gui/res/cardsfolder/h/haunted_house.txt create mode 100644 forge-gui/res/cardsfolder/k/kiddie_coaster.txt create mode 100644 forge-gui/res/cardsfolder/m/merry_go_round.txt create mode 100644 forge-gui/res/cardsfolder/r/roller_coaster.txt create mode 100644 forge-gui/res/cardsfolder/s/spinny_ride.txt create mode 100644 forge-gui/res/cardsfolder/t/trash_bin.txt diff --git a/forge-gui/res/cardsfolder/b/bounce_chamber.txt b/forge-gui/res/cardsfolder/b/bounce_chamber.txt new file mode 100644 index 00000000000..1c996e8a688 --- /dev/null +++ b/forge-gui/res/cardsfolder/b/bounce_chamber.txt @@ -0,0 +1,11 @@ +Name:Bounce Chamber +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:TrigChoose +SVar:TrigChoose:DB$ ChooseCard | Choices$ Creature.YouDontCtrl+leastToughnessControlledByOpponent | ChoiceTitle$ Choose a creature with the least toughness among creatures your opponents control | Mandatory$ True | SubAbility$ DBBounce | SpellDescription$ Return a creature you don’t control with the lowest toughness among creatures you don’t control to its owner’s hand. +SVar:DBBounce:DB$ ChangeZone | Defined$ ChosenCard | Origin$ Battlefield | Destination$ Hand +Oracle:Visit — Return a creature you don’t control with the lowest toughness among creatures you don’t control to its owner’s hand. (If multiple creatures are tied, choose any one of them.) \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/f/foam_weapons_kiosk.txt b/forge-gui/res/cardsfolder/f/foam_weapons_kiosk.txt new file mode 100644 index 00000000000..6e18c4edaac --- /dev/null +++ b/forge-gui/res/cardsfolder/f/foam_weapons_kiosk.txt @@ -0,0 +1,12 @@ +Name:Foam Weapons Kiosk +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:TrigCounter +SVar:TrigCounter:DB$ PutCounter | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature | CounterType$ P1P1 | CounterNum$ 1 | SubAbility$ DBPump | SpellDescription$ Put a +1/+1 counter on target creature you control. It gains Vigilance until end of turn. +SVar:DBPump:DB$ Pump | Defined$ Targeted | KW$ Vigilance +DeckHas:Ability$Counters +Oracle:Visit — Put a +1/+1 counter on target creature you control. That creature gains vigilance until end of turn. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/h/haunted_house.txt b/forge-gui/res/cardsfolder/h/haunted_house.txt new file mode 100644 index 00000000000..dcf801414d2 --- /dev/null +++ b/forge-gui/res/cardsfolder/h/haunted_house.txt @@ -0,0 +1,9 @@ +Name:Haunted House +ManaCost:no cost +Types:Artifact Attraction +Variant:A:Lights:3 6 +Variant:B:Lights:4 6 +K:Visit:TrigRes +SVar:TrigRes:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.YouOwn | TgtPrompt$ Choose target creature in your graveyard. | SubAbility$ DBHaste | SpellDescription$ Return target creature card from your graveyard to the battlefield. It gains haste. Exile it at the beginning of the next end step. +SVar:DBHaste:DB$ Animate | Defined$ Targeted | Keywords$ Haste | Duration$ Permanent | AtEOT$ YourExile +Oracle:Visit — Return target creature card from your graveyard to the battlefield. It gains haste. Exile it at the beginning of your next end step. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/k/kiddie_coaster.txt b/forge-gui/res/cardsfolder/k/kiddie_coaster.txt new file mode 100644 index 00000000000..06bbe352cad --- /dev/null +++ b/forge-gui/res/cardsfolder/k/kiddie_coaster.txt @@ -0,0 +1,12 @@ +Name:Kiddie Coaster +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:TrigPump +SVar:TrigPump:DB$ PumpAll | ValidCards$ Creature.YouCtrl | NumAtt$ +1 | SpellDescription$ Creatures you control get +1/+0 until end of turn. +Oracle:Visit — Creatures you control get +1/+0 until end of turn. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/m/merry_go_round.txt b/forge-gui/res/cardsfolder/m/merry_go_round.txt new file mode 100644 index 00000000000..e60b774959a --- /dev/null +++ b/forge-gui/res/cardsfolder/m/merry_go_round.txt @@ -0,0 +1,8 @@ +Name:Merry-Go-Round +ManaCost:no cost +Types:Artifact Attraction +Variant:A:Lights:2 6 +Variant:B:Lights:5 6 +K:Visit:TrigPump +SVar:TrigPump:DB$ PumpAll | ValidCards$ Creature.powerLE2+YouCtrl | KW$ Horsemanship | SpellDescription$ Creatures you control with power 2 or less gain horsemanship until end of turn. +Oracle:Visit — Creatures you control with power 2 or less gain horsemanship until end of turn. (They can’t be blocked except by creatures with horsemanship.) \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/r/roller_coaster.txt b/forge-gui/res/cardsfolder/r/roller_coaster.txt new file mode 100644 index 00000000000..78adbecbe54 --- /dev/null +++ b/forge-gui/res/cardsfolder/r/roller_coaster.txt @@ -0,0 +1,10 @@ +Name:Roller Coaster +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:TrigPump +SVar:TrigPump:DB$ PumpAll | ValidCards$ Creature.YouCtrl | NumAtt$ +2 | SpellDescription$ Creatures you control get +2/+0 until end of turn. +Oracle:Visit — Creatures you control get +2/+0 until end of turn. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/s/spinny_ride.txt b/forge-gui/res/cardsfolder/s/spinny_ride.txt new file mode 100644 index 00000000000..6da7297cdd3 --- /dev/null +++ b/forge-gui/res/cardsfolder/s/spinny_ride.txt @@ -0,0 +1,12 @@ +Name:Spinny Ride +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:TrigTap +SVar:TrigTap:DB$ Tap | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Choose target creature an opponent controls. | SpellDescription$ Tap target creature an opponent controls. +Oracle:Visit — Tap target creature an opponent controls. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/t/trash_bin.txt b/forge-gui/res/cardsfolder/t/trash_bin.txt new file mode 100644 index 00000000000..95357cbe7dc --- /dev/null +++ b/forge-gui/res/cardsfolder/t/trash_bin.txt @@ -0,0 +1,12 @@ +Name:Trash Bin +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:TrigMill +SVar:TrigMill:DB$ Mill | NumCards$ 2 | SubAbility$ DBReturn | SpellDescription$ Mill two cards, then return a creature card from your graveyard to your hand. +SVar:DBReturn:DB$ ChangeZone | Origin$ Graveyard | Destination$ Hand | ChangeType$ Card.YouOwn | AtRandom$ True | Hidden$ True +DeckHas:Ability$Graveyard|Mill +Oracle:Visit — Mill two cards, then return a card at random from your graveyard to your hand. (To mill a card, a player puts the top card of their library into their graveyard.) \ No newline at end of file From 85d42bfbcf7136350d96f2bb079d246273677c96 Mon Sep 17 00:00:00 2001 From: Jetz Date: Sun, 23 Jun 2024 11:20:06 -0400 Subject: [PATCH 15/20] Update for "visited this turn" per rules --- .../java/forge/game/ability/AbilityUtils.java | 4 ---- .../src/main/java/forge/game/card/Card.java | 14 ++++++++++++++ .../java/forge/game/card/CardProperty.java | 4 ++++ .../main/java/forge/game/player/Player.java | 18 +----------------- .../res/cardsfolder/s/squirrel_squatters.txt | 2 +- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index b232a2baf99..ff49b2d0a5b 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -2659,10 +2659,6 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { return game.getPhaseHandler().getPlanarDiceSpecialActionThisTurn(); } - if (sq[0].startsWith("AttractionsYouVisitedThisTurn")) { - return doXMath(player.getAttractionsVisitedThisTurn(), expr, c, ctb); - } - if (sq[0].equals("AllTypes")) { List cards = getDefinedCards(c, sq[1], ctb); 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 09e728b6ad9..f9235856451 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -223,6 +223,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private int timesSaddledThisTurn = 0; private CardCollection saddledByThisTurn; + private boolean visitedThisTurn = false; + private int classLevel = 1; private long bestowTimestamp = -1; private long transformedTimestamp = 0; @@ -6613,6 +6615,17 @@ public final void setCrewedByThisTurn(final CardCollection crew) { crewedByThisTurn = crew; } + public final void visitAttraction(Player visitor) { + this.visitedThisTurn = true; + + final Map runParams = AbilityKey.mapFromCard(this); + runParams.put(AbilityKey.Player, visitor); + game.getTriggerHandler().runTrigger(TriggerType.VisitAttraction, runParams, false); + } + public final boolean wasVisitedThisTurn() { + return this.visitedThisTurn; + } + public final int getClassLevel() { return classLevel; } @@ -7123,6 +7136,7 @@ public void onCleanupPhase(final Player turn) { resetExertedThisTurn(); resetCrewed(); resetSaddled(); + visitedThisTurn = false; resetChosenModeTurn(); resetAbilityResolvedThisTurn(); } diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index f8423f91411..91aba9c5839 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -1947,6 +1947,10 @@ else if (property.equals("blocked")) { } } else if (property.equals("SaddledThisTurn")) { if (!hasTimestampMatch(card, source.getSaddledByThisTurn())) return false; + } else if (property.equals("VisitedThisTurn")) { + if (!card.wasVisitedThisTurn()) { + return false; + } } else if (property.equals("IsSuspected")) { if (!card.isSuspected()) { return false; 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 f49e67bc6e9..e3b82339e4a 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -165,8 +165,6 @@ 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; @@ -1954,12 +1952,6 @@ public final void resetRingTemptedYou() { 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()) { @@ -2543,8 +2535,6 @@ public void onCleanupPhase() { damageReceivedThisTurn.clear(); planeswalkedToThisTurn.clear(); - attractionsVisitedThisTurn = 0; - // set last turn nr if (game.getPhaseHandler().isPlayerTurn(this)) { setBeenDealtCombatDamageSinceLastTurn(false); @@ -3841,14 +3831,8 @@ public void setCommitedCrimeThisTurn(int 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.mapFromPlayer(this); - runParams.put(AbilityKey.Card, c); - game.getTriggerHandler().runTrigger(TriggerType.VisitAttraction, runParams, false); + c.visitAttraction(this); } } public void rollToVisitAttractions() { diff --git a/forge-gui/res/cardsfolder/s/squirrel_squatters.txt b/forge-gui/res/cardsfolder/s/squirrel_squatters.txt index 855c49333bc..bd1ce77b826 100644 --- a/forge-gui/res/cardsfolder/s/squirrel_squatters.txt +++ b/forge-gui/res/cardsfolder/s/squirrel_squatters.txt @@ -6,6 +6,6 @@ T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefi SVar:TrigOpenAttraction:DB$ OpenAttraction T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigToken | TriggerDescription$ Whenever CARDNAME attacks, create a 1/1 green Squirrel creature token that's tapped and attacking for each Attraction you've visited this turn. SVar:TrigToken:DB$ Token | TokenAmount$ X | TokenScript$ g_1_1_squirrel | TokenOwner$ You | TokenTapped$ True | TokenAttacking$ True -SVar:X:Count$AttractionsYouVisitedThisTurn +SVar:X:Count$Valid Attraction.VisitedThisTurn SVar:HasAttackEffect:TRUE Oracle:When Squirrel Squatters enters the battlefield, open an Attraction. (Put the top card of your Attraction deck onto the battlefield.)\nWhenever Squirrel Squatters attacks, create a 1/1 green Squirrel creature token that’s tapped and attacking for each Attraction you’ve visited this turn. \ No newline at end of file From 1233f2bec9c095db0fba730f7cadfe8afc0bc3ab Mon Sep 17 00:00:00 2001 From: Jetz Date: Sun, 23 Jun 2024 11:30:25 -0400 Subject: [PATCH 16/20] Remove "Result:" prefix for notification messages with no source. --- forge-game/src/main/java/forge/util/MessageUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-game/src/main/java/forge/util/MessageUtil.java b/forge-game/src/main/java/forge/util/MessageUtil.java index 051bdd77e7b..3190a4f89a4 100644 --- a/forge-game/src/main/java/forge/util/MessageUtil.java +++ b/forge-game/src/main/java/forge/util/MessageUtil.java @@ -30,7 +30,7 @@ public static String formatMessage(String message, PlayerView player, Object rel // These are not much related to PlayerController public static String formatNotificationMessage(SpellAbility sa, Player player, GameObject target, String value) { if (sa == null || sa.getApi() == null || sa.getHostCard() == null) { - return Localizer.getInstance().getMessage("lblResultIs", value); + return String.valueOf(value); } String choser = StringUtils.capitalize(mayBeYou(player, target)); switch(sa.getApi()) { From f2cefc8cc16539314ec5148be1708423f930338e Mon Sep 17 00:00:00 2001 From: Jetz Date: Sun, 23 Jun 2024 11:36:13 -0400 Subject: [PATCH 17/20] Intervention for AI's gambling problem. --- forge-gui/res/cardsfolder/e/everythingamajig.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/forge-gui/res/cardsfolder/e/everythingamajig.txt b/forge-gui/res/cardsfolder/e/everythingamajig.txt index ecc98832f31..53386bd4b68 100644 --- a/forge-gui/res/cardsfolder/e/everythingamajig.txt +++ b/forge-gui/res/cardsfolder/e/everythingamajig.txt @@ -6,5 +6,6 @@ Variant:C:SVar:DBAddMana:DB$ Mana | Produced$ C | Amount$ 2 Variant:C:A:AB$ Discard | Cost$ 3 T | ValidTgts$ Player | NumCards$ 1 | Mode$ TgtChoose | PlayerTurn$ True | SpellDescription$ Target player discards a card. Variant:C:A:AB$ Animate | Cost$ X | Defined$ Self | Power$ X | Toughness$ X | Types$ Creature,Artifact,Construct | RemoveCreatureTypes$ True | SpellDescription$ CARDNAME becomes an X/X Construct artifact creature until end of turn. Variant:C:SVar:X:Count$xPaid +AI:RemoveDeck:All Oracle: Variant:C:Oracle:{1}: Flip a coin. If you win the flip, add {C}{C}. Activate only as an instant.\n{3}, {T}: Target player discards a card. Activate only during your turn.\n{X}: Everythingamajig becomes an X/X Construct artifact creature until end of turn. \ No newline at end of file From cfc33fd49196252ab7bfe49e1df05e1198b5566f Mon Sep 17 00:00:00 2001 From: Jetz Date: Tue, 25 Jun 2024 17:17:14 -0400 Subject: [PATCH 18/20] Cleanup a couple comments --- forge-game/src/main/java/forge/game/card/CardFactoryUtil.java | 2 +- forge-gui-mobile/src/forge/itemmanager/ItemManager.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) 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 f5e6fb6e69a..4432d124943 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -1981,7 +1981,7 @@ public static void addTriggerAbility(final KeywordInterface inst, final Card car } else if (keyword.startsWith("Prize")) { final String[] k = keyword.split(":"); - SpellAbility sa = AbilityFactory.getAbility(card, k[1]); //Is this the right thing? + SpellAbility sa = AbilityFactory.getAbility(card, k[1]); String descStr = "Prize — " + sa.getDescription(); final String trigStr = "Mode$ ClaimPrize | Static$ True | TriggerZones$ Battlefield | ValidCard$ Card.Self" + diff --git a/forge-gui-mobile/src/forge/itemmanager/ItemManager.java b/forge-gui-mobile/src/forge/itemmanager/ItemManager.java index c16a32bd67c..e3f3a42263d 100644 --- a/forge-gui-mobile/src/forge/itemmanager/ItemManager.java +++ b/forge-gui-mobile/src/forge/itemmanager/ItemManager.java @@ -107,7 +107,6 @@ public abstract class ItemManager extends FContainer im * ItemManager Constructor. * * @param genericType0 the class of item that this table will contain - * @param statLabels0 stat labels for this item manager * @param wantUnique0 whether this table should display only one item with the same name */ protected ItemManager(final Class genericType0, final boolean wantUnique0) { @@ -754,7 +753,7 @@ public void applyAdvancedSearchFilter(String filterString) { } /** - * Programmatically method to set this ItemManager's advanced search filter value. + * Programmatic method to set this ItemManager's advanced search filter value. * Other filters will be cleared. */ public void applyAdvancedSearchFilter(String[] filterStrings, boolean joinAnd) { From 611e97706dbe9ecf1d58d58b0d8f548d69330617 Mon Sep 17 00:00:00 2001 From: Jetz Date: Mon, 1 Jul 2024 19:38:33 -0400 Subject: [PATCH 19/20] Lifetime Pass Holder and RolledToVisitAttraction trigger condition. Consolidated rollToVisitAttractions logic back into RollDiceEffect. --- .../java/forge/game/ability/AbilityKey.java | 1 + .../game/ability/effects/RollDiceEffect.java | 35 +++++++---- .../main/java/forge/game/player/Player.java | 63 +------------------ .../forge/game/trigger/TriggerRolledDie.java | 4 ++ .../game/trigger/TriggerRolledDieOnce.java | 4 ++ .../cardsfolder/l/lifetime_pass_holder.txt | 10 +++ 6 files changed, 43 insertions(+), 74 deletions(-) create mode 100644 forge-gui/res/cardsfolder/l/lifetime_pass_holder.txt diff --git a/forge-game/src/main/java/forge/game/ability/AbilityKey.java b/forge-game/src/main/java/forge/game/ability/AbilityKey.java index 9ce196b4af7..c4f3d45ef14 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityKey.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityKey.java @@ -117,6 +117,7 @@ public enum AbilityKey { ReplacementResult("ReplacementResult"), ReplacementResultMap("ReplacementResultMap"), Result("Result"), + RolledToVisitAttractions("RolledToVisitAttractions"), RoomName("RoomName"), Scheme("Scheme"), ScryBottom("ScryBottom"), 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 57b01dd5670..728a80d8d24 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 @@ -68,9 +68,13 @@ protected String getStackDescription(SpellAbility sa) { } public static int rollDiceForPlayer(SpellAbility sa, Player player, int amount, int sides) { - return rollDiceForPlayer(sa, player, amount, sides, 0, 0, null); + boolean toVisitAttractions = sa != null && sa.hasParam("ToVisitYourAttractions"); + return rollDiceForPlayer(sa, player, amount, sides, 0, 0, null, toVisitAttractions); } - private static int rollDiceForPlayer(SpellAbility sa, Player player, int amount, int sides, int ignore, int modifier, List rollsResult) { + public static int rollDiceForPlayerToVisitAttractions(Player player) { + return rollDiceForPlayer(null, player, 1, 6, 0, 0, null, true); + } + private static int rollDiceForPlayer(SpellAbility sa, Player player, int amount, int sides, int ignore, int modifier, List rollsResult, boolean toVisitAttractions) { Map ignoreChosenMap = Maps.newHashMap(); final Map repParams = AbilityKey.mapFromAffected(player); @@ -84,6 +88,7 @@ private static int rollDiceForPlayer(SpellAbility sa, Player player, int amount, case Updated: { amount = (int) repParams.get(AbilityKey.Number); ignore = (int) repParams.get(AbilityKey.Ignore); + //noinspection unchecked ignoreChosenMap = (Map) repParams.get(AbilityKey.IgnoreChosen); break; } @@ -130,7 +135,7 @@ private static int rollDiceForPlayer(SpellAbility sa, Player player, int amount, if (amount > 0) { StringBuilder sb = new StringBuilder(); String rollResults = StringUtils.join(naturalRolls, ", "); - String resultMessage = sa.hasParam("ToVisitYourAttractions") ? "lblAttractionRollResult" : "lblPlayerRolledResult"; + String resultMessage = toVisitAttractions ? "lblAttractionRollResult" : "lblPlayerRolledResult"; sb.append(Localizer.getInstance().getMessage(resultMessage, player, rollResults)); if (!ignored.isEmpty()) { sb.append("\r\n").append(Localizer.getInstance().getMessage("lblIgnoredRolls", @@ -158,15 +163,17 @@ private static int rollDiceForPlayer(SpellAbility sa, Player player, int amount, countMaxRolls++; } } - if (sa.hasParam("EvenOddResults")) { - sa.setSVar("EvenResults", Integer.toString(evenResults)); - sa.setSVar("OddResults", Integer.toString(oddResults)); - } - if (sa.hasParam("DifferentResults")) { - sa.setSVar("DifferentResults", Integer.toString(differentResults)); - } - if (sa.hasParam("MaxRollsResults")) { - sa.setSVar("MaxRolls", Integer.toString(countMaxRolls)); + if (sa != null) { + if (sa.hasParam("EvenOddResults")) { + sa.setSVar("EvenResults", Integer.toString(evenResults)); + sa.setSVar("OddResults", Integer.toString(oddResults)); + } + if (sa.hasParam("DifferentResults")) { + sa.setSVar("DifferentResults", Integer.toString(differentResults)); + } + if (sa.hasParam("MaxRollsResults")) { + sa.setSVar("MaxRolls", Integer.toString(countMaxRolls)); + } } total += modifier; @@ -177,6 +184,7 @@ private static int rollDiceForPlayer(SpellAbility sa, Player player, int amount, runParams.put(AbilityKey.Sides, sides); runParams.put(AbilityKey.Modifier, modifier); runParams.put(AbilityKey.Result, roll); + runParams.put(AbilityKey.RolledToVisitAttractions, toVisitAttractions); runParams.put(AbilityKey.Number, player.getNumRollsThisTurn() - amount + rollNum); player.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDie, runParams, false); rollNum++; @@ -184,6 +192,7 @@ private static int rollDiceForPlayer(SpellAbility sa, Player player, int amount, final Map runParams = AbilityKey.mapFromPlayer(player); runParams.put(AbilityKey.Sides, sides); runParams.put(AbilityKey.Result, rolls); + runParams.put(AbilityKey.RolledToVisitAttractions, toVisitAttractions); player.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDieOnce, runParams, false); return total; @@ -219,7 +228,7 @@ private int rollDice(SpellAbility sa, Player player, int amount, int sides) { final int ignore = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("IgnoreLower", "0"), sa); List rolls = new ArrayList<>(); - int total = rollDiceForPlayer(sa, player, amount, sides, ignore, modifier, rolls); + int total = rollDiceForPlayer(sa, player, amount, sides, ignore, modifier, rolls, sa.hasParam("ToVisitYourAttractions")); if (sa.hasParam("UseHighestRoll")) { total = Collections.max(rolls); 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 19992f4c9ca..a04ec0cf677 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -33,6 +33,7 @@ import forge.game.ability.AbilityKey; import forge.game.ability.ApiType; import forge.game.ability.effects.DetachedCardEffect; +import forge.game.ability.effects.RollDiceEffect; import forge.game.card.*; import forge.game.card.CardPredicates.Presets; import forge.game.event.*; @@ -3835,67 +3836,7 @@ public void visitAttractions(int light) { } } 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); + this.visitAttractions(RollDiceEffect.rollDiceForPlayerToVisitAttractions(this)); } public void addDeclaresAttackers(long ts, Player p) { diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerRolledDie.java b/forge-game/src/main/java/forge/game/trigger/TriggerRolledDie.java index 80c4554c7e8..e173ad43dad 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerRolledDie.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerRolledDie.java @@ -24,6 +24,10 @@ public final boolean performTest(final Map runParams) { if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) { return false; } + if (hasParam("RolledToVisitAttractions")) { + if (!(boolean) runParams.getOrDefault(AbilityKey.RolledToVisitAttractions, false)) + return false; + } if (hasParam("ValidResult")) { String[] params = getParam("ValidResult").split(","); int result = (int) runParams.get(AbilityKey.Result); diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerRolledDieOnce.java b/forge-game/src/main/java/forge/game/trigger/TriggerRolledDieOnce.java index cfe0d94db6a..b551dd09a29 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerRolledDieOnce.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerRolledDieOnce.java @@ -21,6 +21,10 @@ public final boolean performTest(final Map runParams) { if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) { return false; } + if (hasParam("RolledToVisitAttractions")) { + if (!(boolean) runParams.getOrDefault(AbilityKey.RolledToVisitAttractions, false)) + return false; + } return true; } diff --git a/forge-gui/res/cardsfolder/l/lifetime_pass_holder.txt b/forge-gui/res/cardsfolder/l/lifetime_pass_holder.txt new file mode 100644 index 00000000000..93c8602d4fe --- /dev/null +++ b/forge-gui/res/cardsfolder/l/lifetime_pass_holder.txt @@ -0,0 +1,10 @@ +Name:"Lifetime" Pass Holder +ManaCost:B +Types:Creature Zombie Guest +PT:2/1 +K:CARDNAME enters the battlefield tapped. +T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigOpenAttraction | TriggerDescription$ When CARDNAME dies, open an Attraction. +SVar:TrigOpenAttraction:DB$ OpenAttraction +T:Mode$ RolledDie | TriggerZones$ Graveyard | Execute$ TrigReturn | ValidResult$ 6 | RolledToVisitAttractions$ True | ValidPlayer$ You | TriggerDescription$ Whenever you roll to visit your Attractions, if you roll a 6, you may return CARDNAME from your graveyard to the battlefield. +SVar:TrigReturn:DB$ ChangeZone | Defined$ Self | Origin$ Graveyard | Destination$ Battlefield +Oracle:"Lifetime" Pass Holder enters the battlefield tapped.\nWhen "Lifetime" Pass Holder dies, open an Attraction.\nWhenever you roll to visit your Attractions, if you roll a 6, you may return "Lifetime" Pass Holder from your graveyard to the battlefield. \ No newline at end of file From c346fb02c5fd28e1567091927f2a6d9f0e5a5c21 Mon Sep 17 00:00:00 2001 From: Jetz Date: Tue, 2 Jul 2024 09:22:09 -0400 Subject: [PATCH 20/20] Remove unused import. --- forge-game/src/main/java/forge/game/player/Player.java | 1 - 1 file changed, 1 deletion(-) 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 a04ec0cf677..2a7bb7bf4c3 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -62,7 +62,6 @@ 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;