diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java index b433fd3588f..3a69cfd0b37 100644 --- a/forge-core/src/main/java/forge/card/CardEdition.java +++ b/forge-core/src/main/java/forge/card/CardEdition.java @@ -1018,16 +1018,13 @@ public final class CardEdition implements Comparable { public static final Predicate HAS_BOOSTER_BOX = edition -> edition.getBoosterBoxCount() > 0; + @Deprecated //Use CardEdition::hasBasicLands and a nonnull test. public static final Predicate hasBasicLands = ed -> { if (ed == null) { // Happens for new sets with "???" code return false; } - for(String landName : MagicColor.Constant.BASIC_LANDS) { - if (null == StaticData.instance().getCommonCards().getCard(landName, ed.getCode(), 0)) - return false; - } - return true; + return ed.hasBasicLands(); }; } @@ -1048,7 +1045,7 @@ public final class CardEdition implements Comparable { public boolean hasBasicLands() { for(String landName : MagicColor.Constant.BASIC_LANDS) { - if (null == StaticData.instance().getCommonCards().getCard(landName, this.getCode(), 0)) + if (this.getCardInSet(landName).isEmpty()) return false; } return true; diff --git a/forge-core/src/main/java/forge/deck/DeckRecognizer.java b/forge-core/src/main/java/forge/deck/DeckRecognizer.java index 2757f418b26..ab2cc78a0f0 100644 --- a/forge-core/src/main/java/forge/deck/DeckRecognizer.java +++ b/forge-core/src/main/java/forge/deck/DeckRecognizer.java @@ -49,6 +49,16 @@ public class DeckRecognizer { LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET, CARD_FROM_INVALID_SET, + /** + * Valid card request, but can't be imported because the player does not have enough copies. + * Should be replaced with a different printing if possible. + */ + CARD_NOT_IN_INVENTORY, + /** + * Valid card request for a card that isn't in the player's inventory, but new copies can be acquired freely. + * Usually used for basic lands. Should be supplied to the import controller by the editor. + */ + FREE_CARD_NOT_IN_INVENTORY, // Warning messages WARNING_MESSAGE, UNKNOWN_CARD, @@ -63,10 +73,14 @@ public class DeckRecognizer { CARD_TYPE, CARD_RARITY, CARD_CMC, - MANA_COLOUR + MANA_COLOUR; + + public static final EnumSet CARD_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET, CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY); + public static final EnumSet IN_DECK_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, DECK_NAME, FREE_CARD_NOT_IN_INVENTORY); + public static final EnumSet CARD_PLACEHOLDER_TOKEN_TYPES = EnumSet.of(CARD_TYPE, CARD_RARITY, CARD_CMC, MANA_COLOUR); } - public enum LimitedCardType{ + public enum LimitedCardType { BANNED, RESTRICTED, } @@ -108,6 +122,10 @@ public class DeckRecognizer { return new Token(TokenType.CARD_FROM_INVALID_SET, count, card, cardRequestHasSetCode); } + public static Token NotInInventoryFree(final PaperCard card, final int count, final DeckSection section) { + return new Token(TokenType.FREE_CARD_NOT_IN_INVENTORY, count, card, section, true); + } + // WARNING MESSAGES // ================ public static Token UnknownCard(final String cardName, final String setCode, final int count) { @@ -126,6 +144,10 @@ public class DeckRecognizer { return new Token(TokenType.WARNING_MESSAGE, msg); } + public static Token NotInInventory(final PaperCard card, final int count, final DeckSection section) { + return new Token(TokenType.CARD_NOT_IN_INVENTORY, count, card, section, false); + } + /* ================================= * DECK SECTIONS * ================================= */ @@ -239,14 +261,11 @@ public class DeckRecognizer { /** * Filters all token types that have a PaperCard instance set (not null) * @return true for tokens of type: - * LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET. + * LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY. * False otherwise. */ public boolean isCardToken() { - return (this.type == TokenType.LEGAL_CARD || - this.type == TokenType.LIMITED_CARD || - this.type == TokenType.CARD_FROM_NOT_ALLOWED_SET || - this.type == TokenType.CARD_FROM_INVALID_SET); + return TokenType.CARD_TOKEN_TYPES.contains(this.type); } /** @@ -255,9 +274,7 @@ public class DeckRecognizer { * LEGAL_CARD, LIMITED_CARD, DECK_NAME; false otherwise. */ public boolean isTokenForDeck() { - return (this.type == TokenType.LEGAL_CARD || - this.type == TokenType.LIMITED_CARD || - this.type == TokenType.DECK_NAME); + return TokenType.IN_DECK_TOKEN_TYPES.contains(this.type); } /** @@ -266,7 +283,7 @@ public class DeckRecognizer { * False otherwise. */ public boolean isCardTokenForDeck() { - return (this.type == TokenType.LEGAL_CARD || this.type == TokenType.LIMITED_CARD); + return isCardToken() && isTokenForDeck(); } /** @@ -276,10 +293,7 @@ public class DeckRecognizer { * CARD_RARITY, CARD_CMC, CARD_TYPE, MANA_COLOUR */ public boolean isCardPlaceholder(){ - return (this.type == TokenType.CARD_RARITY || - this.type == TokenType.CARD_CMC || - this.type == TokenType.MANA_COLOUR || - this.type == TokenType.CARD_TYPE); + return TokenType.CARD_PLACEHOLDER_TOKEN_TYPES.contains(this.type); } /** Determines if current token is a Deck Section token @@ -536,7 +550,7 @@ public class DeckRecognizer { PaperCard tokenCard = token.getCard(); if (isAllowed(tokenSection)) { - if (!tokenSection.equals(referenceDeckSectionInParsing)) { + if (tokenSection != referenceDeckSectionInParsing) { Token sectionToken = Token.DeckSection(tokenSection.name(), this.allowedDeckSections); // just check that last token is stack is a card placeholder. // In that case, add the new section token before the placeholder @@ -575,7 +589,7 @@ public class DeckRecognizer { refLine = purgeAllLinks(refLine); String line; - if (StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER)) + if (refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER)) line = refLine.replaceAll(LINE_COMMENT_DELIMITER_OR_MD_HEADER, ""); else line = refLine.trim(); // Remove any trailing formatting @@ -584,7 +598,7 @@ public class DeckRecognizer { // Final fantasy cards like Summon: Choco/Mog should be ommited to be recognized. TODO: fix maybe for future cards if (!line.contains("Summon:")) line = SEARCH_SINGLE_SLASH.matcher(line).replaceFirst(" // "); - if (StringUtils.startsWith(line, ASTERISK)) // markdown lists (tappedout md export) + if (line.startsWith(ASTERISK)) // Markdown lists (tappedout md export) line = line.substring(2); // == Patches to Corner Cases @@ -600,8 +614,8 @@ public class DeckRecognizer { Token result = recogniseCardToken(line, referenceSection); if (result == null) result = recogniseNonCardToken(line); - return result != null ? result : StringUtils.startsWith(refLine, DOUBLE_SLASH) || - StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER) ? + return result != null ? result : refLine.startsWith(DOUBLE_SLASH) || + refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER) ? new Token(TokenType.COMMENT, 0, refLine) : new Token(TokenType.UNKNOWN_TEXT, 0, refLine); } @@ -613,7 +627,7 @@ public class DeckRecognizer { while (m.find()) { line = line.replaceAll(m.group(), "").trim(); } - if (StringUtils.endsWith(line, "()")) + if (line.endsWith("()")) return line.substring(0, line.length()-2); return line; } @@ -741,21 +755,12 @@ public class DeckRecognizer { // This would save tons of time in parsing Input + would also allow to return UnsupportedCardTokens beforehand private DeckSection getTokenSection(String deckSec, DeckSection currentDeckSection, PaperCard card){ if (deckSec != null) { - DeckSection cardSection; - switch (deckSec.toUpperCase().trim()) { - case "MB": - cardSection = DeckSection.Main; - break; - case "SB": - cardSection = DeckSection.Sideboard; - break; - case "CM": - cardSection = DeckSection.Commander; - break; - default: - cardSection = DeckSection.matchingSection(card); - break; - } + DeckSection cardSection = switch (deckSec.toUpperCase().trim()) { + case "MB" -> DeckSection.Main; + case "SB" -> DeckSection.Sideboard; + case "CM" -> DeckSection.Commander; + default -> DeckSection.matchingSection(card); + }; if (cardSection.validate(card)) return cardSection; } @@ -1017,51 +1022,21 @@ public class DeckRecognizer { private static MagicColor.Color getMagicColor(String colorName){ if (colorName.toLowerCase().startsWith("multi") || colorName.equalsIgnoreCase("m")) return null; // will be handled separately - - byte color = MagicColor.fromName(colorName.toLowerCase()); - switch (color) { - case MagicColor.WHITE: - return MagicColor.Color.WHITE; - case MagicColor.BLUE: - return MagicColor.Color.BLUE; - case MagicColor.BLACK: - return MagicColor.Color.BLACK; - case MagicColor.RED: - return MagicColor.Color.RED; - case MagicColor.GREEN: - return MagicColor.Color.GREEN; - default: - return MagicColor.Color.COLORLESS; - - } + return MagicColor.Color.fromByte(MagicColor.fromName(colorName.toLowerCase())); } public static String getLocalisedMagicColorName(String colorName){ Localizer localizer = Localizer.getInstance(); - switch(colorName.toLowerCase()){ - case MagicColor.Constant.WHITE: - return localizer.getMessage("lblWhite"); - - case MagicColor.Constant.BLUE: - return localizer.getMessage("lblBlue"); - - case MagicColor.Constant.BLACK: - return localizer.getMessage("lblBlack"); - - case MagicColor.Constant.RED: - return localizer.getMessage("lblRed"); - - case MagicColor.Constant.GREEN: - return localizer.getMessage("lblGreen"); - - case MagicColor.Constant.COLORLESS: - return localizer.getMessage("lblColorless"); - case "multicolour": - case "multicolor": - return localizer.getMessage("lblMulticolor"); - default: - return ""; - } + return switch (colorName.toLowerCase()) { + case MagicColor.Constant.WHITE -> localizer.getMessage("lblWhite"); + case MagicColor.Constant.BLUE -> localizer.getMessage("lblBlue"); + case MagicColor.Constant.BLACK -> localizer.getMessage("lblBlack"); + case MagicColor.Constant.RED -> localizer.getMessage("lblRed"); + case MagicColor.Constant.GREEN -> localizer.getMessage("lblGreen"); + case MagicColor.Constant.COLORLESS -> localizer.getMessage("lblColorless"); + case "multicolour", "multicolor" -> localizer.getMessage("lblMulticolor"); + default -> ""; + }; } /** @@ -1080,37 +1055,6 @@ public class DeckRecognizer { return ""; } - - - private static Pair getManaNameAndSymbol(String matchedMana) { - if (matchedMana == null) - return null; - - Localizer localizer = Localizer.getInstance(); - switch (matchedMana.toLowerCase()) { - case MagicColor.Constant.WHITE: - case "w": - return Pair.of(localizer.getMessage("lblWhite"), MagicColor.Color.WHITE.getSymbol()); - case MagicColor.Constant.BLUE: - case "u": - return Pair.of(localizer.getMessage("lblBlue"), MagicColor.Color.BLUE.getSymbol()); - case MagicColor.Constant.BLACK: - case "b": - return Pair.of(localizer.getMessage("lblBlack"), MagicColor.Color.BLACK.getSymbol()); - case MagicColor.Constant.RED: - case "r": - return Pair.of(localizer.getMessage("lblRed"), MagicColor.Color.RED.getSymbol()); - case MagicColor.Constant.GREEN: - case "g": - return Pair.of(localizer.getMessage("lblGreen"), MagicColor.Color.GREEN.getSymbol()); - case MagicColor.Constant.COLORLESS: - case "c": - return Pair.of(localizer.getMessage("lblColorless"), MagicColor.Color.COLORLESS.getSymbol()); - default: // Multicolour - return Pair.of(localizer.getMessage("lblMulticolor"), ""); - } - } - public static boolean isDeckName(final String lineAsIs) { if (lineAsIs == null) return false; diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/AddBasicLandsDialog.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/AddBasicLandsDialog.java index 7cff8386751..e2aa334956a 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/AddBasicLandsDialog.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/AddBasicLandsDialog.java @@ -68,7 +68,7 @@ public class AddBasicLandsDialog { private static final int LAND_PANEL_PADDING = 3; private final FComboBoxPanel cbLandSet = new FComboBoxPanel<>(Localizer.getInstance().getMessage("lblLandSet") + ":", FlowLayout.CENTER, - IterableUtil.filter(StaticData.instance().getSortedEditions(), CardEdition.Predicates.hasBasicLands)); + IterableUtil.filter(StaticData.instance().getSortedEditions(), CardEdition::hasBasicLands)); private final MainPanel panel = new MainPanel(); private final LandPanel pnlPlains = new LandPanel("Plains"); diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java index a572911f12a..54bfaff992e 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java @@ -46,7 +46,6 @@ import forge.toolbox.*; import forge.util.Localizer; import forge.view.FDialog; import net.miginfocom.swing.MigLayout; -import org.apache.commons.lang3.StringUtils; import static forge.deck.DeckRecognizer.TokenType.*; @@ -523,7 +522,7 @@ public class DeckImport extends FDialog { else deck.setName(currentDeckName); } - host.getDeckController().loadDeck(deck, controller.getCreateNewDeck()); + host.getDeckController().loadDeck(deck, controller.getImportBehavior() != DeckImportController.ImportBehavior.MERGE); processWindowEvent(new WindowEvent(DeckImport.this, WindowEvent.WINDOW_CLOSING)); }); @@ -531,7 +530,7 @@ public class DeckImport extends FDialog { this.createNewDeckCheckbox.setSelected(false); this.createNewDeckCheckbox.addActionListener(e -> { boolean createNewDeck = createNewDeckCheckbox.isSelected(); - controller.setCreateNewDeck(createNewDeck); + controller.setImportBehavior(createNewDeck ? DeckImportController.ImportBehavior.CREATE_NEW : DeckImportController.ImportBehavior.MERGE); String cmdAcceptLabel = createNewDeck ? CREATE_NEW_DECK_CMD_LABEL : IMPORT_CARDS_CMD_LABEL; cmdAcceptButton.setText(cmdAcceptLabel); String smartCardArtChboxTooltip = createNewDeck ? SMART_CARDART_TT_NO_DECK : SMART_CARDART_TT_WITH_DECK; @@ -600,7 +599,7 @@ public class DeckImport extends FDialog { if (token.getType() == LIMITED_CARD) cssClass = WARN_MSG_CLASS; String statusMsg = String.format("%s", cssClass, - getTokenStatusMessage(token)); + controller.getTokenStatusMessage(token)); statusLbl.append(statusMsg); } @@ -740,12 +739,12 @@ public class DeckImport extends FDialog { private String toHTML(final DeckRecognizer.Token token) { if (token == null) return ""; - String tokenMsg = getTokenMessage(token); + String tokenMsg = controller.getTokenMessage(token); if (tokenMsg == null) return ""; - String tokenStatus = getTokenStatusMessage(token); + String tokenStatus = controller.getTokenStatusMessage(token); String cssClass = getTokenCSSClass(token.getType()); - if (tokenStatus.length() == 0) + if (tokenStatus.isEmpty()) tokenMsg = padEndWithHTMLSpaces(tokenMsg, 2*PADDING_TOKEN_MSG_LENGTH+10); else { tokenMsg = padEndWithHTMLSpaces(tokenMsg, PADDING_TOKEN_MSG_LENGTH); @@ -755,11 +754,6 @@ public class DeckImport extends FDialog { tokenMsg = String.format("%s", cssClass, token.getKey().toString(), tokenMsg); - if (tokenStatus == null) { - String tokenTag = String.format("%s", cssClass, tokenMsg); - return String.format("%s", tokenTag); - } - String tokenTag = "%s"; String tokenMsgTag = String.format(tokenTag, cssClass, tokenMsg); String tokenStatusTag; @@ -776,97 +770,6 @@ public class DeckImport extends FDialog { return String.format("%s%s", targetMsg, spacer); } - private String getTokenMessage(DeckRecognizer.Token token) { - switch (token.getType()) { - case LEGAL_CARD: - case LIMITED_CARD: - case CARD_FROM_NOT_ALLOWED_SET: - case CARD_FROM_INVALID_SET: - return String.format("%s x %s %s", token.getQuantity(), token.getText(), getTokenFoilLabel(token)); - // Card Warning Msgs - case UNKNOWN_CARD: - case UNSUPPORTED_CARD: - return token.getQuantity() > 0 ? String.format("%s x %s", token.getQuantity(), token.getText()) - : token.getText(); - - case UNSUPPORTED_DECK_SECTION: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblWarningMsgPrefix"), - Localizer.getInstance() - .getMessage("lblWarnDeckSectionNotAllowedInEditor", token.getText(), - this.currentGameType)); - - // Special Case of Card moved into another section (e.g. Commander from Sideboard) - case WARNING_MESSAGE: - return String.format("%s: %s", Localizer.getInstance() - .getMessage("lblWarningMsgPrefix"), token.getText()); - - // Placeholders - case DECK_SECTION_NAME: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblDeckSection"), - token.getText()); - - case CARD_RARITY: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblRarity"), - token.getText()); - - case CARD_TYPE: - case CARD_CMC: - case MANA_COLOUR: - case COMMENT: - return token.getText(); - - case DECK_NAME: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblDeckName"), - token.getText()); - - case UNKNOWN_TEXT: - default: - return null; - - } - } - - private String getTokenStatusMessage(DeckRecognizer.Token token){ - if (token == null) - return ""; - - switch (token.getType()) { - case LIMITED_CARD: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblWarningMsgPrefix"), - Localizer.getInstance().getMessage("lblWarnLimitedCard", - StringUtils.capitalize(token.getLimitedCardType().name()), getGameFormatLabel())); - - case CARD_FROM_NOT_ALLOWED_SET: - return Localizer.getInstance().getMessage("lblErrNotAllowedCard", getGameFormatLabel()); - - case CARD_FROM_INVALID_SET: - return Localizer.getInstance().getMessage("lblErrCardEditionDate"); - - case UNSUPPORTED_CARD: - return Localizer.getInstance().getMessage("lblErrUnsupportedCard", this.currentGameType); - - case UNKNOWN_CARD: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblWarningMsgPrefix"), - Localizer.getInstance().getMessage("lblWarnUnknownCardMsg")); - - case UNSUPPORTED_DECK_SECTION: - case WARNING_MESSAGE: - case COMMENT: - case CARD_CMC: - case MANA_COLOUR: - case CARD_TYPE: - case DECK_SECTION_NAME: - case CARD_RARITY: - case DECK_NAME: - case LEGAL_CARD: - case UNKNOWN_TEXT: - default: - return ""; - - } - - } - private String getTokenCSSClass(DeckRecognizer.TokenType tokenType){ switch (tokenType){ case LEGAL_CARD: @@ -899,17 +802,6 @@ public class DeckImport extends FDialog { return ""; } } - - private String getTokenFoilLabel(DeckRecognizer.Token token) { - if (!token.isCardToken()) - return ""; - final String foilMarker = "- (Foil)"; - return token.getCard().isFoil() ? foilMarker : ""; - } - - private String getGameFormatLabel() { - return String.format("\"%s\"", this.controller.getCurrentGameFormatName()); - } } class GameFormatDropdownRenderer extends JLabel implements ListCellRenderer { diff --git a/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java b/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java index f67b249c031..808b366c3ac 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java +++ b/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java @@ -27,8 +27,6 @@ import forge.item.PaperCard; import forge.itemmanager.*; import forge.itemmanager.filters.CardColorFilter; import forge.itemmanager.filters.CardTypeFilter; -import forge.localinstance.properties.ForgePreferences; -import forge.menu.FCheckBoxMenuItem; import forge.menu.FDropDownMenu; import forge.menu.FMenuItem; import forge.menu.FPopupMenu; @@ -41,6 +39,7 @@ import forge.util.Utils; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; public class AdventureDeckEditor extends FDeckEditor { protected static class AdventureEditorConfig extends DeckEditorConfig { @@ -146,7 +145,8 @@ public class AdventureDeckEditor extends FDeckEditor { if(event.cardBlock != null) { if(event.cardBlock.getLandSet() != null) return List.of(event.cardBlock.getLandSet()); - List eventSets = event.cardBlock.getSets(); + List eventSets = new ArrayList<>(event.cardBlock.getSets()); + eventSets.removeIf(Predicate.not(CardEdition::hasBasicLands)); if(!eventSets.isEmpty()) return eventSets; } @@ -558,7 +558,7 @@ public class AdventureDeckEditor extends FDeckEditor { currentEvent.participants[i].setDeck(opponentDecks[i]); } currentEvent.draftedDeck = (Deck) currentEvent.registeredDeck.copyTo("Draft Deck"); - if (allowsAddBasic()) { + if (allowAddBasic()) { showAddBasicLandsDialog(); //Might be annoying if you haven't pruned your deck yet, but best to remind player that //this probably needs to be done since it's there since it's not normally part of Adventure @@ -713,27 +713,6 @@ public class AdventureDeckEditor extends FDeckEditor { return this.deckHeader; } - @Override - protected FPopupMenu createMoreOptionsMenu() { - return new FPopupMenu() { - @Override - protected void buildMenu() { - Localizer localizer = Forge.getLocalizer(); - addItem(new FMenuItem(localizer.getMessage("btnCopyToClipboard"), Forge.hdbuttons ? FSkinImage.HDEXPORT : FSkinImage.BLANK, e1 -> FDeckViewer.copyDeckToClipboard(getDeck()))); - if (allowsAddBasic()) { - FMenuItem addBasic = new FMenuItem(localizer.getMessage("lblAddBasicLands"), FSkinImage.LANDLOGO, e1 -> showAddBasicLandsDialog()); - addItem(addBasic); - } - if(FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.DEV_MODE_ENABLED)) { - addItem(new FCheckBoxMenuItem(localizer.getMessage("cbEnforceDeckLegality"), shouldEnforceConformity(), e -> toggleConformity())); - String devSuffix = " (" + localizer.getMessage("lblDev") + ")"; - addItem(new FMenuItem(localizer.getMessage("lblAddcard") + devSuffix, FSkinImage.HDPLUS, e -> showDevAddCardDialog())); - } - ((DeckEditorPage) getSelectedPage()).buildDeckMenu(this); - } - }; - } - @Override protected void addChosenBasicLands(CardPool landsToAdd) { if(isLimitedEditor()) @@ -765,6 +744,12 @@ public class AdventureDeckEditor extends FDeckEditor { catalog.moveCards(landsToMove, getMainDeckPage()); } + @Override + protected PaperCard supplyPrintForImporter(PaperCard missingCard) { + PaperCard out = super.supplyPrintForImporter(missingCard); + return out == null ? null : out.getNoSellVersion(); + } + @Override protected void cacheTabPages() { super.cacheTabPages(); @@ -775,7 +760,9 @@ public class AdventureDeckEditor extends FDeckEditor { } @Override - protected boolean allowsAddBasic() { + protected boolean allowAddBasic() { + if(getEditorConfig() instanceof DeckPreviewConfig) + return false; AdventureEventData currentEvent = getCurrentEvent(); if (currentEvent == null) return true; diff --git a/forge-gui-mobile/src/forge/deck/AddBasicLandsDialog.java b/forge-gui-mobile/src/forge/deck/AddBasicLandsDialog.java index ca0d93ff361..d7709e5cee8 100644 --- a/forge-gui-mobile/src/forge/deck/AddBasicLandsDialog.java +++ b/forge-gui-mobile/src/forge/deck/AddBasicLandsDialog.java @@ -59,7 +59,7 @@ public class AddBasicLandsDialog extends FDialog { private final Consumer callback; private final FLabel lblLandSet = add(new FLabel.Builder().text(Forge.getLocalizer().getMessage("lblLandSet") + ":").font(FSkinFont.get(12)).textColor(FLabel.getInlineLabelColor()).build()); - private final FComboBox cbLandSet = add(new FComboBox<>(IterableUtil.filter(StaticData.instance().getEditions(), CardEdition.Predicates.hasBasicLands))); + private final FComboBox cbLandSet = add(new FComboBox<>(IterableUtil.filter(StaticData.instance().getSortedEditions(), CardEdition::hasBasicLands))); private final FScrollPane scroller = add(new FScrollPane() { @Override diff --git a/forge-gui-mobile/src/forge/deck/FDeckEditor.java b/forge-gui-mobile/src/forge/deck/FDeckEditor.java index 41cb685e509..eff2481bff0 100644 --- a/forge-gui-mobile/src/forge/deck/FDeckEditor.java +++ b/forge-gui-mobile/src/forge/deck/FDeckEditor.java @@ -81,13 +81,21 @@ public class FDeckEditor extends TabPageScreen { public boolean allowsCardReplacement() { return hasInfiniteCardPool() || usePlayerInventory(); } public List getBasicLandSets(Deck currentDeck) { + if(hasInfiniteCardPool()) + return FModel.getMagicDb().getSortedEditions().stream().filter(CardEdition::hasBasicLands).collect(Collectors.toList()); return List.of(DeckProxy.getDefaultLandSet(currentDeck)); } protected abstract IDeckController getController(); protected abstract DeckEditorPage[] getInitialPages(); - protected DeckSection[] getExtraSections() { + public DeckSection[] getPrimarySections() { + if(getGameType() != null) + return getGameType().getPrimaryDeckSections().toArray(new DeckSection[0]); + return new DeckSection[]{DeckSection.Main, DeckSection.Sideboard}; + } + + public DeckSection[] getExtraSections() { if(getGameType() != null) return getGameType().getSupplimentalDeckSections().toArray(new DeckSection[0]); return new DeckSection[]{DeckSection.Attractions, DeckSection.Contraptions}; @@ -144,7 +152,7 @@ public class FDeckEditor extends TabPageScreen { ItemManagerConfig catalogConfig = null; ItemManagerConfig mainSectionConfig = null; ItemManagerConfig sideboardConfig = null; - Function fnGetBasicLandSet = null; + Function> fnGetBasicLandSet = null; Supplier> itemPoolSupplier = null; String catalogCaption = null; @@ -196,7 +204,7 @@ public class FDeckEditor extends TabPageScreen { this.sideboardConfig = sideboardConfig; return this; } - public GameTypeDeckEditorConfig setBasicLandSetFunction(Function fnGetBasicLandSet) { + public GameTypeDeckEditorConfig setBasicLandSetFunction(Function> fnGetBasicLandSet) { this.fnGetBasicLandSet = fnGetBasicLandSet; return this; } @@ -296,9 +304,21 @@ public class FDeckEditor extends TabPageScreen { } @Override - protected DeckSection[] getExtraSections() { + public DeckSection[] getPrimarySections() { + return gameType.getPrimaryDeckSections().toArray(new DeckSection[0]); + } + + @Override + public DeckSection[] getExtraSections() { return gameType.getSupplimentalDeckSections().toArray(new DeckSection[0]); } + + @Override + public List getBasicLandSets(Deck currentDeck) { + if(this.fnGetBasicLandSet != null) + return List.copyOf(fnGetBasicLandSet.apply(currentDeck)); + return super.getBasicLandSets(currentDeck); + } } public static DeckEditorConfig EditorConfigConstructed = new GameTypeDeckEditorConfig(GameType.Constructed, @@ -348,18 +368,19 @@ public class FDeckEditor extends TabPageScreen { .setMainSectionConfig(ItemManagerConfig.QUEST_DECK_EDITOR) .setSideboardConfig(ItemManagerConfig.QUEST_DECK_EDITOR) .setPlayerInventorySupplier(() -> FModel.getQuest().getCards().getCardpool()) - .setBasicLandSetFunction(d -> FModel.getQuest().getDefaultLandSet()); + .setBasicLandSetFunction(d -> FModel.getQuest().getAvailableLandSets()); public static DeckEditorConfig EditorConfigQuestCommander = new GameTypeDeckEditorConfig(GameType.QuestCommander, DECK_CONTROLLER_QUEST) .setCatalogConfig(ItemManagerConfig.QUEST_EDITOR_POOL) .setMainSectionConfig(ItemManagerConfig.QUEST_DECK_EDITOR) .setSideboardConfig(ItemManagerConfig.QUEST_DECK_EDITOR) .setPlayerInventorySupplier(() -> FModel.getQuest().getCards().getCardpool()) - .setBasicLandSetFunction(d -> FModel.getQuest().getDefaultLandSet()); + .setBasicLandSetFunction(d -> FModel.getQuest().getAvailableLandSets()); public static DeckEditorConfig EditorConfigQuestDraft = new GameTypeDeckEditorConfig(GameType.QuestDraft, DECK_CONTROLLER_QUEST_DRAFT); public static DeckEditorConfig EditorConfigPlanarConquest = new GameTypeDeckEditorConfig(GameType.PlanarConquest, DECK_CONTROLLER_PLANAR_CONQUEST) .setCatalogConfig(ItemManagerConfig.CONQUEST_COLLECTION) .setMainSectionConfig(ItemManagerConfig.CONQUEST_DECK_EDITOR) - .setPlayerInventorySupplier(ConquestUtil::getAvailablePool); + .setPlayerInventorySupplier(ConquestUtil::getAvailablePool) + .setBasicLandSetFunction(ConquestUtil::getBasicLandSets); protected static DeckSectionPage createPageForExtraSection(DeckSection deckSection, DeckEditorConfig editorConfig) { CardManager cm = new CardManager(false); @@ -542,7 +563,7 @@ public class FDeckEditor extends TabPageScreen { @Override protected void buildMenu() { final Localizer localizer = Forge.getLocalizer(); - if (allowsAddBasic()) + if (allowAddBasic()) addItem(new FMenuItem(localizer.getMessage("lblAddBasicLands"), FSkinImage.LANDLOGO, e -> showAddBasicLandsDialog())); if (showAddExtraSectionOption()) { addItem(new FMenuItem(localizer.getMessage("lblAddDeckSection"), FSkinImage.CHAOS, e -> { @@ -558,28 +579,41 @@ public class FDeckEditor extends TabPageScreen { }); })); } - if (editorConfig.getGameType() != null && editorConfig.hasInfiniteCardPool()) { + if (editorConfig.hasInfiniteCardPool() || editorConfig.usePlayerInventory()) { addItem(new FMenuItem(localizer.getMessage("lblImportFromClipboard"), Forge.hdbuttons ? FSkinImage.HDIMPORT : FSkinImage.OPEN, e -> { - FDeckImportDialog dialog = new FDeckImportDialog(!deck.isEmpty(), FDeckEditor.this.editorConfig); + FDeckImportDialog dialog = new FDeckImportDialog(deck, FDeckEditor.this.editorConfig); + if(editorConfig.usePlayerInventory()) + dialog.setFreePrintConverter(FDeckEditor.this::supplyPrintForImporter); + dialog.setImportBannedCards(!FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)); dialog.setCallback(importedDeck -> { if (deck != null && importedDeck.hasName()) { deck.setName(importedDeck.getName()); setHeaderText(importedDeck.getName()); } - if (dialog.createNewDeck()) { - for (Entry section : importedDeck) { - DeckSectionPage page = getPageForSection(section.getKey()); - if (page != null) - page.setCards(section.getValue()); - } - } else { - for (Entry section : importedDeck) { - DeckSectionPage page = getPageForSection(section.getKey()); - if (page != null) - page.addCards(section.getValue()); - } + switch (dialog.getImportBehavior()) { + case REPLACE_CURRENT: + for(DeckSectionPage page : pagesBySection.values()) { + if(importedDeck.has(page.deckSection)) { + page.setCards(importedDeck.get(page.deckSection)); + if(hiddenExtraSections.contains(page.deckSection)) + showExtraSectionTab(page.deckSection); + } + else + page.setCards(new CardPool()); + } + break; + case CREATE_NEW: + deckController.setDeck(importedDeck); + break; + case MERGE: + for (Entry section : importedDeck) { + DeckSectionPage page = getPageForSection(section.getKey()); + if (page != null) + page.addCards(section.getValue()); + } } }); + dialog.initParse(); dialog.show(); setSelectedPage(getMainDeckPage()); //select main deck page if needed so main deck if visible below dialog })); @@ -643,6 +677,20 @@ public class FDeckEditor extends TabPageScreen { getMainDeckPage().addCards(landsToAdd); } + /** + * If a card is missing from a player's inventory while importing a deck, it gets run through here. + * Returning a PaperCard will let unlimited copies of that card be used as a substitute. Returning null + * will leave the card missing from the import. + */ + protected PaperCard supplyPrintForImporter(PaperCard missingCard) { + //Could support dungeons here too? Not that we really use them in the editor... + if(!missingCard.isVeryBasicLand()) + return null; + List basicSets = editorConfig.getBasicLandSets(deck); + String setCode = basicSets.isEmpty() ? "JMP" : basicSets.get(0).getCode(); + return FModel.getMagicDb().fetchCard(missingCard.getCardName(), setCode, null); + } + protected boolean shouldEnforceConformity() { if(FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)) return true; @@ -695,6 +743,9 @@ public class FDeckEditor extends TabPageScreen { showExtraSectionTab(section); if(pagesBySection.containsKey(section)) setSelectedPage(pagesBySection.get(section)); + else if(section == DeckSection.Main && pagesBySection.containsKey(mainDeckPage.deckSection)) + //Tried to switch to the Main page in a Planar or Scheme deck. + setSelectedPage(pagesBySection.get(mainDeckPage.deckSection)); } public void notifyNewControllerModel() { @@ -1027,7 +1078,7 @@ public class FDeckEditor extends TabPageScreen { protected boolean allowSaveAs() { return allowSave() && allowRename(); } - protected boolean allowsAddBasic() { + protected boolean allowAddBasic() { return !isDrafting(); } diff --git a/forge-gui-mobile/src/forge/deck/FDeckImportDialog.java b/forge-gui-mobile/src/forge/deck/FDeckImportDialog.java index 9287adfcfa6..251da9f6716 100644 --- a/forge-gui-mobile/src/forge/deck/FDeckImportDialog.java +++ b/forge-gui-mobile/src/forge/deck/FDeckImportDialog.java @@ -18,11 +18,12 @@ package forge.deck; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; import forge.Forge; import forge.Graphics; @@ -31,11 +32,15 @@ import forge.deck.DeckRecognizer.TokenType; import forge.game.GameType; import forge.gui.FThreads; import forge.gui.util.SOptionPane; +import forge.item.PaperCard; import forge.toolbox.FCheckBox; import forge.toolbox.FComboBox; import forge.toolbox.FDialog; import forge.toolbox.FOptionPane; import forge.toolbox.FTextArea; +import forge.util.ItemPool; +import forge.util.Localizer; +import org.apache.commons.lang3.StringUtils; public class FDeckImportDialog extends FDialog { @@ -45,7 +50,7 @@ public class FDeckImportDialog extends FDialog { private final FCheckBox newEditionCheck = add(new FCheckBox(Forge.getLocalizer().getMessage("lblImportLatestVersionCard"), false)); private final FCheckBox dateTimeCheck = add(new FCheckBox(Forge.getLocalizer().getMessage("lblUseOnlySetsReleasedBefore"), false)); private final FCheckBox smartCardArtCheck = add(new FCheckBox(Forge.getLocalizer().getMessage("lblUseSmartCardArt"), false)); - private final FCheckBox createNewDeckCheck = add(new FCheckBox(Forge.getLocalizer().getMessage("lblNewDeckCheckbox"), false)); + private final FCheckBox createNewDeckCheck = add(new FCheckBox(Forge.getLocalizer().getMessage("lblReplaceDeckCheckbox"), false)); // private final FCheckBox importInDeck = add(new FCheckBox() /*setting onlyCoreExpCheck to false allow the copied cards to pass the check of deck contents forge-core\src\main\java\forge\deck\Deck.javaDeck.java starting @ Line 320 which is called by @@ -57,100 +62,60 @@ public class FDeckImportDialog extends FDialog { private final FComboBox monthDropdown = add(new FComboBox<>()); //don't need wrappers since skin can't change while this dialog is open private final FComboBox yearDropdown = add(new FComboBox<>()); - private final boolean showOptions; - private final boolean currentDeckIsEmpty; + private boolean showOptions; + private final Deck currentDeck; private boolean createNewDeckControl; private final DeckImportController controller; + private final FDeckEditor.DeckEditorConfig editorConfig; private final static ImmutableList importOrCancel = ImmutableList.of(Forge.getLocalizer().getMessage("lblImport"), Forge.getLocalizer().getMessage("lblCancel")); - public FDeckImportDialog(final boolean replacingDeck, final FDeckEditor.DeckEditorConfig editorConfig) { + public FDeckImportDialog(final Deck currentDeck, final FDeckEditor.DeckEditorConfig editorConfig) { super(Forge.getLocalizer().getMessage("lblImportFromClipboard"), 2); + boolean usingInventory = editorConfig.usePlayerInventory(); + boolean replacingDeck = !currentDeck.isEmpty() || usingInventory; + this.currentDeck = currentDeck; + this.editorConfig = editorConfig; + ItemPool cardPool = editorConfig.getCardPool(false); controller = new DeckImportController(dateTimeCheck, monthDropdown, yearDropdown, replacingDeck); String contents = Forge.getClipboard().getContents(); if (contents == null) contents = ""; //prevent NPE txtInput.setText(contents); - if (editorConfig.allowsCardReplacement()) { - GameType gameType = editorConfig.getGameType(); - controller.setGameFormat(gameType); - List supportedSections = new ArrayList<>(); - supportedSections.add(DeckSection.Main); - supportedSections.add(DeckSection.Sideboard); - if (editorConfig.hasCommander()) - supportedSections.add(DeckSection.Commander); - supportedSections.addAll(Lists.newArrayList(editorConfig.getExtraSections())); - controller.setAllowedSections(supportedSections); - } + GameType gameType = editorConfig.getGameType(); + controller.setGameFormat(gameType); + List supportedSections = new ArrayList<>(); + supportedSections.addAll(List.of(editorConfig.getPrimarySections())); + supportedSections.addAll(List.of(editorConfig.getExtraSections())); + controller.setAllowedSections(supportedSections); + controller.setCurrentDeckInEditor(currentDeck); + if(usingInventory) + controller.setPlayerInventory(cardPool); onlyCoreExpCheck.setSelected(StaticData.instance().isCoreExpansionOnlyFilterSet()); newEditionCheck.setSelected(StaticData.instance().cardArtPreferenceIsLatest()); smartCardArtCheck.setSelected(StaticData.instance().isEnabledCardArtSmartSelection()); createNewDeckCheck.setSelected(replacingDeck); - this.currentDeckIsEmpty = !replacingDeck; this.createNewDeckControl = replacingDeck; - initButton(0, Forge.getLocalizer().getMessage("lblImport"), e -> FThreads.invokeInBackgroundThread(() -> { - List tokens = controller.parseInput(txtInput.getText()); //ensure deck updated based on any changes to options + if(usingInventory) + controller.setImportBehavior(DeckImportController.ImportBehavior.REPLACE_CURRENT); + else + controller.setImportBehavior(createNewDeckControl ? DeckImportController.ImportBehavior.CREATE_NEW : DeckImportController.ImportBehavior.MERGE); - if (controller.isSmartCardArtEnabled()) - tokens = controller.optimiseCardArtInTokens(); - - //if there are any cards that cannot be imported, let user know this and give them the option to cancel - StringBuilder sb = new StringBuilder(); - for (DeckRecognizer.Token token : tokens) { - if (token.getType() == TokenType.CARD_FROM_NOT_ALLOWED_SET - || token.getType() == TokenType.CARD_FROM_INVALID_SET - || token.getType() == TokenType.UNKNOWN_CARD - || token.getType() == TokenType.UNSUPPORTED_CARD) { - if (sb.length() > 0) - sb.append("\n"); - sb.append(token.getQuantity()).append(" ").append(token.getText()); - } - } - if (sb.length() > 0) { - if (SOptionPane.showOptionDialog(Forge.getLocalizer().getMessage("lblFollowingCardsCannotBeImported") + "\n\n" + sb, Forge.getLocalizer().getMessage("lblImportRemainingCards"), SOptionPane.INFORMATION_ICON, importOrCancel) == 1) { - return; - } - } - - final Deck deck = controller.accept(); //must accept in background thread in case a dialog is shown - if (deck == null) { return; } - - FThreads.invokeInEdtLater(() -> { - hide(); - if (callback != null) - callback.accept(deck); - }); - })); + initButton(0, Forge.getLocalizer().getMessage("lblImport"), e -> FThreads.invokeInBackgroundThread(this::performImport)); initButton(1, Forge.getLocalizer().getMessage("lblCancel"), e -> hide()); - List tokens = controller.parseInput(txtInput.getText()); - if (controller.isSmartCardArtEnabled()) - tokens = controller.optimiseCardArtInTokens(); - //ensure at least one known card found on clipboard - for (DeckRecognizer.Token token : tokens) { - if (token.getType() == TokenType.LEGAL_CARD) { - showOptions = true; - - dateTimeCheck.setCommand(e -> updateDropDownEnabled()); - newEditionCheck.setCommand(e -> setArtPreferenceInController()); - onlyCoreExpCheck.setCommand(e -> setArtPreferenceInController()); - smartCardArtCheck.setCommand(e -> controller.setSmartCardArtOptimisation(smartCardArtCheck.isSelected())); - createNewDeckCheck.setCommand(e -> { - createNewDeckControl = createNewDeckCheck.isSelected(); - controller.setCreateNewDeck(createNewDeckControl); - }); - updateDropDownEnabled(); - setArtPreferenceInController(); - return; - } - } - - showOptions = false; - setButtonEnabled(0, false); - txtInput.setText(Forge.getLocalizer().getMessage("lblNoKnownCardsOnClipboard")); + dateTimeCheck.setCommand(e -> updateDropDownEnabled()); + newEditionCheck.setCommand(e -> setArtPreferenceInController()); + onlyCoreExpCheck.setCommand(e -> setArtPreferenceInController()); + smartCardArtCheck.setCommand(e -> controller.setSmartCardArtOptimisation(smartCardArtCheck.isSelected())); + createNewDeckCheck.setCommand(e -> { + createNewDeckControl = createNewDeckCheck.isSelected(); + controller.setImportBehavior(createNewDeckControl ? DeckImportController.ImportBehavior.CREATE_NEW : DeckImportController.ImportBehavior.MERGE); + }); + setShowOptions(false); } private void setArtPreferenceInController() { @@ -160,16 +125,66 @@ public class FDeckImportDialog extends FDialog { } private void updateDropDownEnabled() { - boolean enabled = dateTimeCheck.isSelected(); + boolean enabled = dateTimeCheck.isSelected() && this.showOptions; monthDropdown.setEnabled(enabled); yearDropdown.setEnabled(enabled); } + private void setShowOptions(boolean showOptions) { + this.showOptions = showOptions; + dateTimeCheck.setEnabled(showOptions); + newEditionCheck.setEnabled(showOptions); + onlyCoreExpCheck.setEnabled(showOptions); + newEditionCheck.setEnabled(showOptions); + smartCardArtCheck.setEnabled(showOptions); + createNewDeckCheck.setEnabled(showOptions); + updateDropDownEnabled(); + } + public void setCallback(Consumer callback0){ callback = callback0; } - public boolean createNewDeck(){ return this.createNewDeckControl; } + public void setFreePrintConverter(Function freePrintConverter) { + this.controller.setFreePrintConverter(freePrintConverter); + } + + public DeckImportController.ImportBehavior getImportBehavior() { + return controller.getImportBehavior(); + } + + public void setImportBannedCards(boolean importBannedCards) { + controller.importBannedAndRestrictedCards(importBannedCards); + } + + public void initParse() { + boolean usingInventory = editorConfig.usePlayerInventory(); + List tokens = controller.parseInput(txtInput.getText()); + if (usingInventory) + tokens = controller.constrainTokensToInventory(); + else if (controller.isSmartCardArtEnabled()) + tokens = controller.optimiseCardArtInTokens(); + //ensure at least one known card found on clipboard + for (DeckRecognizer.Token token : tokens) { + if (token.getType() == TokenType.LEGAL_CARD || token.getType() == TokenType.FREE_CARD_NOT_IN_INVENTORY) { + + if(usingInventory) { + //Settings aren't compatible with player inventories. + setShowOptions(false); + return; + } + + setShowOptions(true); + + updateDropDownEnabled(); + setArtPreferenceInController(); + return; + } + } + + setButtonEnabled(0, false); + txtInput.setText(Forge.getLocalizer().getMessage("lblNoKnownCardsOnClipboard")); + } @Override public void drawOverlay(Graphics g) { @@ -202,7 +217,7 @@ public class FDeckImportDialog extends FDialog { yearDropdown.setBounds(x + dropDownWidth + fieldPadding, y, dropDownWidth, h); y += h + fieldPadding; - if (!this.currentDeckIsEmpty){ + if (!this.currentDeck.isEmpty()){ smartCardArtCheck.setBounds(x, y, w/2, h); createNewDeckCheck.setBounds(x + w/2, y, w/2, h); } else @@ -222,4 +237,49 @@ public class FDeckImportDialog extends FDialog { } return y; } + + private static final EnumSet MISSING_TOKENS = EnumSet.of(TokenType.CARD_FROM_NOT_ALLOWED_SET, + TokenType.CARD_FROM_INVALID_SET, TokenType.UNKNOWN_CARD, TokenType.UNSUPPORTED_CARD, + TokenType.WARNING_MESSAGE, TokenType.CARD_NOT_IN_INVENTORY); + + private void performImport() { + List tokens = controller.parseInput(txtInput.getText()); //ensure deck updated based on any changes to options + + if (editorConfig.usePlayerInventory()) + tokens = controller.constrainTokensToInventory(); + else if (controller.isSmartCardArtEnabled()) + tokens = controller.optimiseCardArtInTokens(); + + //if there are any cards that cannot be imported, let user know this and give them the option to cancel + StringBuilder sb = new StringBuilder(); + for (DeckRecognizer.Token token : tokens) { + if (MISSING_TOKENS.contains(token.getType())) { + if (!sb.isEmpty()) + sb.append("\n"); + String message = controller.getTokenMessage(token); + String statusMessage = controller.getTokenStatusMessage(token); + if(!StringUtils.isBlank(statusMessage)) + sb.append(String.format("%s - (%s)", message, statusMessage)); + else + sb.append(statusMessage); + } + } + if (!sb.isEmpty()) { + Localizer localizer = Forge.getLocalizer(); + if (SOptionPane.showOptionDialog(localizer.getMessage("lblFollowingCardsCannotBeImported") + "\n\n" + sb, localizer.getMessage("lblImportRemainingCards"), SOptionPane.WARNING_ICON, importOrCancel) == 1) { + return; + } + } + + final Deck deck = controller.accept(currentDeck.getName()); //must accept in background thread in case a dialog is shown + if (deck == null) { + return; + } + + FThreads.invokeInEdtLater(() -> { + hide(); + if (callback != null) + callback.accept(deck); + }); + } } diff --git a/forge-gui/res/languages/de-DE.properties b/forge-gui/res/languages/de-DE.properties index f1119155fe8..56cbd2770a5 100644 --- a/forge-gui/res/languages/de-DE.properties +++ b/forge-gui/res/languages/de-DE.properties @@ -2838,6 +2838,7 @@ lblDecklistTitle=Deckliste lblSummaryStats=Gesamt-Statistik lblDeckSection=Bereich lblNewDeckCheckbox=Erzeuge ein neues Deck +lblReplaceDeckCheckbox=Replace Current Deck lblImportCardsCmd=Importiere Karten lblCreateNewCmd=Neues Deck lblErrNotAllowedCard=Set ist nicht erlaubt in {0} @@ -2845,6 +2846,7 @@ lblWarnLimitedCard={0} in {1} lblErrCardEditionDate=Set verträgt sich nicht mit der Erscheinungsdatum-Option lblErrUnsupportedCard=Ist nicht erlaubt in {0} lblWarnUnknownCardMsg=Unbekannte oder in Forge nicht unterstützte Karte +lblWarnNotInInventory=Card not found in inventory lblWarnTooManyCommanders=Aktueller {0}-Bereich enthält {1} mögliche Commander-Karten: {2} lblWarnCommandersInSideExtra=Bitte prüfen und, falls nötig, min. eine Karte in den Commander-Bereich verschieben. lblWarnDeckSectionNotAllowedInEditor=In {1} ist der {0}-Bereich nicht erlaubt. @@ -2936,6 +2938,7 @@ lblCardImportWarning=\nWarnung: Das Deck {0} wird umbenannt in {1}. lblConfirmCreateNewDeck=Du bist dabei das neue Deck {0} zu erzeugen. {1}\n\nWillst du fortfahren?\n\n Hinweis: \ Bitte denk daran den "Save"-Knopf im Deck-Editor zu klicken, um das neue Deck dem Deck-Katalog hinzuzufügen! lblNewDeckWarning=\nWarnung: Alle ungesicherten Änderungen am aktuellen Deck {0} werden verlorengehen. +lblConfirmReplaceDeck=This will replace the contents of the current deck ({0}) with the imported cards.\n\nWould you like to proceed? lblImportCardsDialogTitle=Importiere Karten in aktuelles Deck lblNewDeckDialogTitle=Erzeuge neues Deck #FNetOverlay.java diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index f808e0d736b..4b49dcbf189 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -2898,6 +2898,7 @@ lblDecklistTitle=Decklist lblSummaryStats=Summary Statistics lblDeckSection=Section lblNewDeckCheckbox=Create a New Deck +lblReplaceDeckCheckbox=Replace Current Deck lblImportCardsCmd=Import Cards lblCreateNewCmd=New Deck lblErrNotAllowedCard=Set not allowed in {0} @@ -2905,6 +2906,7 @@ lblWarnLimitedCard={0} in {1} lblErrCardEditionDate=Set not compliant with Release Date option lblErrUnsupportedCard=Not allowed in {0} lblWarnUnknownCardMsg=Unknown Card or Unsupported in Forge +lblWarnNotInInventory=Card not found in inventory lblWarnTooManyCommanders=Current {0} Section contains {1} potential Commander Cards: {2} lblWarnCommandersInSideExtra=Please check and move one to the Commander Section, in case. lblWarnDeckSectionNotAllowedInEditor={0} Section is not allowed in {1} @@ -3001,6 +3003,7 @@ lblCardImportWarning=\nWarning: The deck {0} will be renamed as {1}. lblConfirmCreateNewDeck=You are about to create a new deck {0}. {1}\n\nWould you like to proceed?\n\n Note: \ Please remember to click on the "Save" button in the Deck Editor to add the new deck to the Catalog! lblNewDeckWarning=\nWarning: Any unsaved changes to the current deck {0} will be lost. +lblConfirmReplaceDeck=This will replace the contents of the current deck ({0}) with the imported cards.\n\nWould you like to proceed? lblImportCardsDialogTitle=Import cards in the Current Deck lblNewDeckDialogTitle=Create a New Deck #FNetOverlay.java diff --git a/forge-gui/res/languages/es-ES.properties b/forge-gui/res/languages/es-ES.properties index a715861c960..663759c9bdf 100644 --- a/forge-gui/res/languages/es-ES.properties +++ b/forge-gui/res/languages/es-ES.properties @@ -2847,6 +2847,7 @@ lblDecklistTitle=Decklist lblSummaryStats=Summary Statistics lblDeckSection=Section lblNewDeckCheckbox=Create a New Deck +lblReplaceDeckCheckbox=Replace Current Deck lblImportCardsCmd=Import Cards lblCreateNewCmd=New Deck lblErrNotAllowedCard=Set not allowed in {0} @@ -2854,6 +2855,7 @@ lblWarnLimitedCard={0} in {1} lblErrCardEditionDate=Set not compliant with Release Date option lblErrUnsupportedCard=Not allowed in {0} lblWarnUnknownCardMsg=Unknown Card or Unsupported in Forge +lblWarnNotInInventory=Card not found in inventory lblWarnTooManyCommanders=Current {0} Section contains {1} potential Commander Cards: {2} lblWarnCommandersInSideExtra=Please check and move one to the Commander Section, in case. lblWarnDeckSectionNotAllowedInEditor={0} Section is not allowed in {1} @@ -2950,6 +2952,7 @@ lblCardImportWarning=\nWarning: The deck {0} will be renamed as {1}. lblConfirmCreateNewDeck=You are about to create a new deck {0}. {1}\n\nWould you like to proceed?\n\n Note: \ Please remember to click on the "Save" button in the Deck Editor to add the new deck to the Catalog! lblNewDeckWarning=\nWarning: Any unsaved changes to the current deck {0} will be lost. +lblConfirmReplaceDeck=This will replace the contents of the current deck ({0}) with the imported cards.\n\nWould you like to proceed? lblImportCardsDialogTitle=Import cards in the Current Deck lblNewDeckDialogTitle=Create a New Deck #FNetOverlay.java diff --git a/forge-gui/res/languages/fr-FR.properties b/forge-gui/res/languages/fr-FR.properties index f0b1edad4a6..6d1a5e71b96 100644 --- a/forge-gui/res/languages/fr-FR.properties +++ b/forge-gui/res/languages/fr-FR.properties @@ -2840,6 +2840,7 @@ lblDecklistTitle=Liste de deck lblSummaryStats=Statistiques récapitulatives lblDeckSection=Section lblNewDeckCheckbox=Créer un nouveau deck +lblReplaceDeckCheckbox=Replace Current Deck lblImportCardsCmd=Importer des cartes lblCreateNewCmd=Nouveau Deck lblErrNotAllowedCard=Définir non autorisé dans {0} @@ -2847,6 +2848,7 @@ lblWarnLimitedCard={0} dans {1} lblErrCardEditionDate=Set non conforme avec l'option de date de sortie lblErrUnsupportedCard=Non autorisé dans {0} lblWarnUnknownCardMsg=Carte inconnue ou non prise en charge dans Forge +lblWarnNotInInventory=Card not found in inventory lblWarnTooManyCommanders=La section {0} actuelle contient {1} cartes de commandant potentielles : {2} lblWarnCommandersInSideExtra=Veuillez vérifier et en déplacer un vers la section Commandant, au cas où. lblWarnDeckSectionNotAllowedInEditor={0} La section n'est pas autorisée dans {1} @@ -2944,6 +2946,7 @@ lblCardImportWarning=\nAttention : Le deck {0} sera renommé en {1}. lblConfirmCreateNewDeck=Vous êtes sur le point de créer un nouveau deck {0}. {1}\n\nVoulez-vous continuer ?\n\n Remarque : \ N'oubliez pas de cliquer sur le bouton "Enregistrer" dans l'éditeur de deck pour ajouter le nouveau deck au catalogue ! lblNewDeckWarning=\nAttention : Toute modification non enregistrée dans le deck actuel {0} sera perdue. +lblConfirmReplaceDeck=This will replace the contents of the current deck ({0}) with the imported cards.\n\nWould you like to proceed? lblImportCardsDialogTitle=Importer des cartes dans le Deck actuel lblNewDeckDialogTitle=Créer un nouveau deck #FNetOverlay.java diff --git a/forge-gui/res/languages/it-IT.properties b/forge-gui/res/languages/it-IT.properties index 3a64c4aff17..f95f2d0c3e1 100644 --- a/forge-gui/res/languages/it-IT.properties +++ b/forge-gui/res/languages/it-IT.properties @@ -2836,6 +2836,7 @@ lblDecklistTitle=Lista delle Carte da Importare lblSummaryStats=Statistiche Generali lblDeckSection=Sezione lblNewDeckCheckbox=Crea un nuovo mazzo +lblReplaceDeckCheckbox=Replace Current Deck lblImportCardsCmd=Importa le carte lblCreateNewCmd=Nuovo mazzo lblErrNotAllowedCard=Edizione non permessa in {0} @@ -2843,6 +2844,7 @@ lblWarnLimitedCard={0} in {1} lblErrCardEditionDate=Edizione non valida secondo l'opzione sulla data di pubblicazione selezionata lblErrUnsupportedCard=Non Permesso in {0} lblWarnUnknownCardMsg=Carta Sconosciuta, o non supportata in Forge +lblWarnNotInInventory=Card not found in inventory lblWarnTooManyCommanders=La Sezione {0} contiene {1} potenziali carte Commander: {2} lblWarnCommandersInSideExtra=Per favore, controlla e nel caso spostane una nella sezione Commander. lblWarnDeckSectionNotAllowedInEditor={0} Sezione non è permessa in {1} @@ -2942,6 +2944,7 @@ lblCardImportWarning=\nAttenzione: Il mazzo {0} sarà rinominato come {1}. lblConfirmCreateNewDeck=Si sta per creare un nuovo mazzo {0}. {1}\n\nSi desidera procedere?\n\n Nota: \ Non dimenticare di premere il tasto "Salva" una volta importate le carte per aggiungere il nuovo mazzo al catalogo! lblNewDeckWarning=\nAttenzione: Qualsiasi modifica non salvata al mazzo corrente {0} sarà persa. +lblConfirmReplaceDeck=This will replace the contents of the current deck ({0}) with the imported cards.\n\nWould you like to proceed? lblImportCardsDialogTitle=Importa le carte nel mazzo corrente lblNewDeckDialogTitle=Crea un nuovo mazzo #FNetOverlay.java diff --git a/forge-gui/res/languages/ja-JP.properties b/forge-gui/res/languages/ja-JP.properties index b60161c00d3..471d530fa74 100644 --- a/forge-gui/res/languages/ja-JP.properties +++ b/forge-gui/res/languages/ja-JP.properties @@ -2835,6 +2835,7 @@ lblDecklistTitle=Decklist lblSummaryStats=Summary Statistics lblDeckSection=Section lblNewDeckCheckbox=Create a New Deck +lblReplaceDeckCheckbox=Replace Current Deck lblImportCardsCmd=Import Cards lblCreateNewCmd=New Deck lblErrNotAllowedCard=Set not allowed in {0} @@ -2842,6 +2843,7 @@ lblWarnLimitedCard={0} in {1} lblErrCardEditionDate=Set not compliant with Release Date option lblErrUnsupportedCard=Not allowed in {0} lblWarnUnknownCardMsg=Unknown Card or Unsupported in Forge +lblWarnNotInInventory=Card not found in inventory lblWarnTooManyCommanders=Current {0} Section contains {1} potential Commander Cards: {2} lblWarnCommandersInSideExtra=Please check and move one to the Commander Section, in case. lblWarnDeckSectionNotAllowedInEditor={0} Section is not allowed in {1} @@ -2938,6 +2940,7 @@ lblCardImportWarning=\nWarning: The deck {0} will be renamed as {1}. lblConfirmCreateNewDeck=You are about to create a new deck {0}. {1}\n\nWould you like to proceed?\n\n Note: \ Please remember to click on the "Save" button in the Deck Editor to add the new deck to the Catalog! lblNewDeckWarning=\nWarning: Any unsaved changes to the current deck {0} will be lost. +lblConfirmReplaceDeck=This will replace the contents of the current deck ({0}) with the imported cards.\n\nWould you like to proceed? lblImportCardsDialogTitle=Import cards in the Current Deck lblNewDeckDialogTitle=Create a New Deck #FNetOverlay.java diff --git a/forge-gui/res/languages/pt-BR.properties b/forge-gui/res/languages/pt-BR.properties index e296ffda340..1386999433d 100644 --- a/forge-gui/res/languages/pt-BR.properties +++ b/forge-gui/res/languages/pt-BR.properties @@ -2917,6 +2917,7 @@ lblDecklistTitle=Lista de decks lblSummaryStats=Estatísticas Resumidas lblDeckSection=Seção lblNewDeckCheckbox=Criar um Novo deck +lblReplaceDeckCheckbox=Replace Current Deck lblImportCardsCmd=Importar Cartas lblCreateNewCmd=Novo Deck lblErrNotAllowedCard=Coleção não permitida em {0} @@ -2924,6 +2925,7 @@ lblWarnLimitedCard={0} em {1} lblErrCardEditionDate=Coleção não compatível com a opção de Data de Lançamento lblErrUnsupportedCard=Não permitido em {0} lblWarnUnknownCardMsg=Carta desconhecida ou não suportada no Forge +lblWarnNotInInventory=Card not found in inventory lblWarnTooManyCommanders=Seção {0} Atual contém {1} Cartas de Comandante em potencial\: {2} lblWarnCommandersInSideExtra=Verifique e mova um para a seção do Comandante. lblWarnDeckSectionNotAllowedInEditor=Seção {0} não é permitida em {1} @@ -3012,6 +3014,7 @@ Deseja prosseguir?\n\ Nota\: Por favor, lembre-se de clicar no botão "Salvar" no Editor do Deck para adicioná-lo ao Catálogo\! lblNewDeckWarning=\n\ Aviso\: Qualquer alteração não salva no deck atual {0} será perdida. +lblConfirmReplaceDeck=This will replace the contents of the current deck ({0}) with the imported cards.\n\nWould you like to proceed? lblImportCardsDialogTitle=Importar cartas no Deck Atual lblNewDeckDialogTitle=Criar um Novo deck #FNetOverlay.java diff --git a/forge-gui/res/languages/zh-CN.properties b/forge-gui/res/languages/zh-CN.properties index 131b6421519..b1d79ab5f0f 100644 --- a/forge-gui/res/languages/zh-CN.properties +++ b/forge-gui/res/languages/zh-CN.properties @@ -2844,6 +2844,7 @@ lblDecklistTitle=套牌列表 lblSummaryStats=统计摘要 lblDeckSection=部分 lblNewDeckCheckbox=创建一个新套牌 +lblReplaceDeckCheckbox=Replace Current Deck lblImportCardsCmd=导入牌张 lblCreateNewCmd=新建套牌 lblErrNotAllowedCard=系列{0}不被允许 @@ -2851,6 +2852,7 @@ lblWarnLimitedCard={0}中的{1} lblErrCardEditionDate=不符合上市日期选项 lblErrUnsupportedCard={0}不被允许 lblWarnUnknownCardMsg=未知的牌张或未被forge支持的牌张 +lblWarnNotInInventory=Card not found in inventory lblWarnTooManyCommanders=现在{0}部分包含{1}张潜在的指挥官牌张: {2} lblWarnCommandersInSideExtra=如果确实是指挥官,请进行进行检查并将其中的一张移动到指挥官区。 lblWarnDeckSectionNotAllowedInEditor={0}部分中的{1}不被允许 @@ -2928,6 +2930,7 @@ lblCardImportWarning=\n警告:套牌{0}将被重命名为{1}。 lblConfirmCreateNewDeck=你即将创建一个新套牌{0}。{1}\n\n你想要继续吗?\n\n 注意:请记得点击套牌编辑器中的\"保存按钮\"以\ 将新建的套牌保存到目录中! lblNewDeckWarning=\n警告:对于当前套牌{0}的任何未保存更改丢将被丢弃。 +lblConfirmReplaceDeck=This will replace the contents of the current deck ({0}) with the imported cards.\n\nWould you like to proceed? lblImportCardsDialogTitle=将牌张导入到当前套牌 lblNewDeckDialogTitle=创建一个新套牌 #FNetOverlay.java diff --git a/forge-gui/src/main/java/forge/deck/DeckImportController.java b/forge-gui/src/main/java/forge/deck/DeckImportController.java index 8ba686c67ce..dbf89e11c2f 100644 --- a/forge-gui/src/main/java/forge/deck/DeckImportController.java +++ b/forge-gui/src/main/java/forge/deck/DeckImportController.java @@ -13,15 +13,24 @@ import forge.gui.util.SOptionPane; import forge.item.PaperCard; import forge.localinstance.properties.ForgePreferences; import forge.model.FModel; +import forge.util.ItemPool; import forge.util.Localizer; +import forge.util.StreamUtil; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import java.text.DateFormatSymbols; import java.util.*; +import java.util.function.Function; public class DeckImportController { - private boolean createNewDeck; + public enum ImportBehavior { + MERGE, + CREATE_NEW, + REPLACE_CURRENT + } + + private ImportBehavior importBehavior; // Date filter private final ICheckBox dateTimeCheck; private final IComboBox monthDropdown; @@ -30,7 +39,7 @@ public class DeckImportController { private CardDb.CardArtPreference artPreference; private boolean smartCardArt; // Block Preference Filter - private boolean inlcludeBnRInDeck = false; + private boolean includeBnRInDeck = false; private final List tokens = new ArrayList<>(); private final Map cardsInTokens = new HashMap<>(); @@ -38,7 +47,14 @@ public class DeckImportController { private Deck currentDeckInEditor = null; private DeckFormat currentDeckFormat; private GameFormat currentGameFormat; + private GameType currentGameType; private final List allowedSections = new ArrayList<>(); + private ItemPool playerInventory; + /** + * If a free card is missing from a player's inventory (e.g. a basic land), it gets run through this function, which + * can handle creation of a usable print. + */ + private Function freePrintSupplier; public DeckImportController(ICheckBox dateTimeCheck0, IComboBox monthDropdown0, IComboBox yearDropdown0, @@ -54,13 +70,14 @@ public class DeckImportController { */ this.currentDeckNotEmpty = currentDeckNotEmpty; // this option will control the "new deck" action controlled by UI widget - createNewDeck = false; + this.importBehavior = ImportBehavior.MERGE; // Init default parameters this.artPreference = StaticData.instance().getCardArtPreference(); // default this.smartCardArt = StaticData.instance().isEnabledCardArtSmartSelection(); this.currentDeckFormat = null; this.currentGameFormat = null; + this.currentGameType = null; fillDateDropdowns(); } @@ -68,13 +85,23 @@ public class DeckImportController { if (gameType == null){ this.currentGameFormat = null; this.currentDeckFormat = null; + this.currentGameType = null; } else { // get the game format with the same name of current game type (if any) this.currentDeckFormat = gameType.getDeckFormat(); this.currentGameFormat = FModel.getFormats().get(gameType.name()); + this.currentGameType = gameType; } } + public void setPlayerInventory(ItemPool inventory) { + this.playerInventory = inventory; + } + + public void setFreePrintConverter(Function freePrintSupplier) { + this.freePrintSupplier = freePrintSupplier; + } + public void setCurrentDeckInEditor(Deck deckInEditor){ this.currentDeckInEditor = deckInEditor; } @@ -105,11 +132,13 @@ public class DeckImportController { return this.smartCardArt; } - public void setCreateNewDeck(boolean createNewDeck){ - this.createNewDeck = createNewDeck; + public void setImportBehavior(ImportBehavior importBehavior) { + this.importBehavior = importBehavior; } - public boolean getCreateNewDeck() { return this.createNewDeck; } + public ImportBehavior getImportBehavior() { + return importBehavior; + } private void fillDateDropdowns() { DateFormatSymbols dfs = new DateFormatSymbols(); @@ -159,10 +188,10 @@ public class DeckImportController { } public void importBannedAndRestrictedCards(boolean includeBannedAndRestricted){ - this.inlcludeBnRInDeck = includeBannedAndRestricted; + this.includeBnRInDeck = includeBannedAndRestricted; } - public boolean importBannedAndRestrictedCards(){ return this.inlcludeBnRInDeck; } + public boolean importBannedAndRestrictedCards(){ return this.includeBnRInDeck; } public List parseInput(String input) { tokens.clear(); @@ -186,7 +215,7 @@ public class DeckImportController { if (!this.allowedSections.isEmpty()) recognizer.setAllowedDeckSections(this.allowedSections); // Banned and Restricted Card Policy - if (this.inlcludeBnRInDeck) + if (this.includeBnRInDeck) recognizer.forceImportBannedAndRestrictedCards(); String[] lines = input.split("\n"); @@ -196,8 +225,13 @@ public class DeckImportController { if (this.currentGameFormatAllowsCommander()) { List> commanderTokens = getTokensInSection(DeckSection.Commander); - if (commanderTokens.isEmpty()) // Check commanders in Sideboard only if the commander section is empty - checkAndFixCommanderIn(DeckSection.Sideboard); + if (commanderTokens.isEmpty()) { + // Check commanders in Sideboard only if the commander section is empty + if(!getTokensInSection(DeckSection.Sideboard).isEmpty()) + checkAndFixCommanderIn(DeckSection.Sideboard); + else + checkAndFixCommanderIn(DeckSection.Main); + } checkAndFixCommanderIn(DeckSection.Commander); } @@ -309,7 +343,7 @@ public class DeckImportController { } public boolean currentGameFormatAllowsCommander(){ - return this.allowedSections.contains(DeckSection.Commander); + return this.allowedSections.contains(DeckSection.Commander) || this.currentGameType == GameType.PlanarConquest; } public List optimiseCardArtInTokens(){ @@ -332,13 +366,8 @@ public class DeckImportController { else refTokenMap = tokensPerSectionWithSet; - List tokensInSection = refTokenMap.getOrDefault(tokenSection, null); - if (tokensInSection == null) { - tokensInSection = new ArrayList<>(); - tokensInSection.add(token); - refTokenMap.put(tokenSection, tokensInSection); - } else - tokensInSection.add(token); + List tokensInSection = refTokenMap.computeIfAbsent(tokenSection, e -> new ArrayList<>()); + tokensInSection.add(token); } if (tokensPerSectionWithNoSet.isEmpty()) @@ -356,7 +385,7 @@ public class DeckImportController { Map referencePoolPerSection = new HashMap<>(); - if (this.currentDeckNotEmpty && !this.createNewDeck && this.currentDeckInEditor != null){ + if (this.currentDeckNotEmpty && this.importBehavior == ImportBehavior.MERGE && this.currentDeckInEditor != null){ // We will always consider ONLY sections for cards needing art optimisation for (DeckSection section : tokensPerSectionWithNoSet.keySet()){ CardPool cardsInDeck = this.currentDeckInEditor.get(section); @@ -436,6 +465,100 @@ public class DeckImportController { return tokens; } + public List constrainTokensToInventory() { + if(this.playerInventory == null) + return tokens; + CardPool availableInventory = new CardPool(this.playerInventory); + //Map of tokens to the things we're gonna replace them with. + Map> tokenReplacers = new LinkedHashMap<>(); + //If we're adding to our existing deck, ensure we aren't counting the cards already in it. + if(this.importBehavior == ImportBehavior.MERGE && this.currentDeckInEditor != null) + availableInventory.removeAll(this.currentDeckInEditor.getAllCardsInASinglePool(true, true)); + if(this.currentGameType == GameType.PlanarConquest && currentDeckInEditor != null) + availableInventory.removeAllFlat(this.currentDeckInEditor.getCommanders()); + //Step 1: For each token, if it's asking for more copies of a print than we can supply, split the difference out + //into a token that's indifferent to the edition. Reduce available inventory accordingly. + for (Token token : this.tokens) { + if (!token.isCardToken()) + continue; + PaperCard card = token.getCard(); + int requestedAmount = token.getQuantity(); + if (card == null) + continue; + if (token.cardRequestHasNoCode()) { + List list = new ArrayList<>(); + tokenReplacers.put(token, list); + continue; + } + int available = availableInventory.count(card); + if (available <= 0) { + List list = new ArrayList<>(); + tokenReplacers.put(token, list); + continue; + } + int numTaken = Math.min(requestedAmount, available); + availableInventory.remove(card, numTaken); + if (available >= requestedAmount) + continue; + + List list = new ArrayList<>(); + list.add(Token.LegalCard(card, numTaken, token.getTokenSection(), true)); + tokenReplacers.put(token, list); + } + if(tokenReplacers.isEmpty()) + return tokens; //We have every card that was requested. + //Step 2: Try to find alternative prints for the ones that do not request an edition. + int capacity = tokens.size(); + for(Map.Entry> tokenReplacer : tokenReplacers.entrySet()) { + Token token = tokenReplacer.getKey(); + DeckSection tokenSection = token.getTokenSection(); + List replacementList = tokenReplacer.getValue(); + PaperCard card = token.getCard(); + String cardName = card.getName(); + CardPool substitutes = availableInventory.getFilteredPool(c -> c.getName().equals(cardName)); + List> sortedSubstitutes = StreamUtil.stream(substitutes).sorted(Comparator.comparingInt(Map.Entry::getValue)).toList(); + int neededQuantity = token.getQuantity(); + for(Token found : replacementList) { + //If there's an item in the replacement list already it means we've already found some of the needed copies. + neededQuantity -= found.getQuantity(); + } + for(int i = 0; i < sortedSubstitutes.size() && neededQuantity > 0; i++) { + Map.Entry item = sortedSubstitutes.get(i); + PaperCard replacement = item.getKey(); + int toMove = Math.min(neededQuantity, item.getValue()); + replacementList.add(Token.LegalCard(replacement, toMove, tokenSection, true)); + availableInventory.remove(replacement, toMove); + neededQuantity -= toMove; + capacity++; + } + if(neededQuantity > 0) { + PaperCard freePrint = getInfiniteSupplyPrinting(card); + if(freePrint != null) + replacementList.add(Token.NotInInventoryFree(freePrint, neededQuantity, tokenSection)); + else + replacementList.add(Token.NotInInventory(card, neededQuantity, tokenSection)); + capacity++; + } + } + //Step 3: Apply the replacement list. + List newList = new ArrayList<>(capacity); + for(Token t : this.tokens) { + if(tokenReplacers.containsKey(t)) + newList.addAll(tokenReplacers.get(t)); + else + newList.add(t); + } + this.tokens.clear(); + this.tokens.addAll(newList); + return tokens; + } + + private PaperCard getInfiniteSupplyPrinting(PaperCard card) { + if(this.freePrintSupplier == null) + return null; + return freePrintSupplier.apply(card); + } + private int countTokens(List tokensInSection){ if (tokensInSection == null || tokensInSection.isEmpty()) return 0; @@ -487,27 +610,31 @@ public class DeckImportController { if (tokens.isEmpty()) { return null; } String deckName = ""; - if (currentDeckName != null && currentDeckName.trim().length() > 0) + if (currentDeckName != null && !currentDeckName.trim().isEmpty()) deckName = String.format("\"%s\"", currentDeckName.trim()); String tokenDeckName = getTokenDeckNameIfAny(); - if (tokenDeckName.length() > 0) + if (!tokenDeckName.isEmpty()) tokenDeckName = String.format("\"%s\"", tokenDeckName); - if (createNewDeck){ - String extraWarning = currentDeckNotEmpty ? localizer.getMessage("lblNewDeckWarning", deckName) : ""; - final String warning = localizer.getMessage("lblConfirmCreateNewDeck", tokenDeckName, extraWarning); - if (!SOptionPane.showConfirmDialog(warning, localizer.getMessage("lblNewDeckDialogTitle"), - localizer.getMessage("lblYes"), localizer.getMessage("lblNo"))) { - return null; + if(this.currentDeckNotEmpty) { + final String warning; + final String title; + if (this.importBehavior == ImportBehavior.CREATE_NEW) { + String extraWarning = localizer.getMessage("lblNewDeckWarning", deckName); + warning = localizer.getMessage("lblConfirmCreateNewDeck", tokenDeckName, extraWarning); + title = localizer.getMessage("lblNewDeckDialogTitle"); + } else if (this.importBehavior == ImportBehavior.MERGE){ + String extraWarning = (!tokenDeckName.isEmpty() && !tokenDeckName.equals(deckName)) ? + localizer.getMessage("lblCardImportWarning", deckName, tokenDeckName) : ""; + warning = localizer.getMessage("lblConfirmCardImport", deckName, extraWarning); + title = localizer.getMessage("lblImportCardsDialogTitle"); } - } - else if (this.currentDeckNotEmpty){ - String extraWarning = (tokenDeckName.length() > 0 && !tokenDeckName.equals(deckName)) ? - localizer.getMessage("lblCardImportWarning", deckName, tokenDeckName) : ""; - final String warning = localizer.getMessage("lblConfirmCardImport", deckName, extraWarning); - if (!SOptionPane.showConfirmDialog(warning, - localizer.getMessage("lblImportCardsDialogTitle"), + else { + warning = localizer.getMessage("lblConfirmReplaceDeck", deckName); + title = localizer.getMessage("lblNewDeckDialogTitle"); + } + if (!SOptionPane.showConfirmDialog(warning, title, localizer.getMessage("lblYes"), localizer.getMessage("lblNo"))) return null; } @@ -516,7 +643,7 @@ public class DeckImportController { final TokenType type = t.getType(); // only Deck Name, legal card and limited card tokens will be analysed! if (!t.isTokenForDeck() || - (type == TokenType.LIMITED_CARD && !this.inlcludeBnRInDeck)) + (type == TokenType.LIMITED_CARD && !this.includeBnRInDeck)) continue; // SKIP token if (type == TokenType.DECK_NAME) { @@ -547,4 +674,66 @@ public class DeckImportController { } return ""; // no deck name } + + public String getTokenMessage(DeckRecognizer.Token token) { + return switch (token.getType()) { + case LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET, CARD_FROM_INVALID_SET, + CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY -> + String.format("%s x %s %s", token.getQuantity(), token.getText(), getTokenFoilLabel(token)); + // Card Warning Msgs + case UNKNOWN_CARD, UNSUPPORTED_CARD -> + token.getQuantity() > 0 ? String.format("%s x %s", token.getQuantity(), token.getText()) + : token.getText(); + case UNSUPPORTED_DECK_SECTION -> + String.format("%s: %s", Localizer.getInstance().getMessage("lblWarningMsgPrefix"), + Localizer.getInstance() + .getMessage("lblWarnDeckSectionNotAllowedInEditor", token.getText(), + this.currentGameType.name())); + + // Special Case of Card moved into another section (e.g. Commander from Sideboard) + case WARNING_MESSAGE -> String.format("%s: %s", Localizer.getInstance() + .getMessage("lblWarningMsgPrefix"), token.getText()); + + // Placeholders + case DECK_SECTION_NAME -> String.format("%s: %s", Localizer.getInstance().getMessage("lblDeckSection"), + token.getText()); + case CARD_RARITY -> String.format("%s: %s", Localizer.getInstance().getMessage("lblRarity"), + token.getText()); + case CARD_TYPE, CARD_CMC, MANA_COLOUR, COMMENT, UNKNOWN_TEXT -> token.getText(); + case DECK_NAME -> String.format("%s: %s", Localizer.getInstance().getMessage("lblDeckName"), + token.getText()); + }; + } + + public String getTokenStatusMessage(DeckRecognizer.Token token) { + if (token == null) + return ""; + + final Localizer localizer = Localizer.getInstance(); + return switch (token.getType()) { + case LIMITED_CARD -> String.format("%s: %s", localizer.getMessage("lblWarningMsgPrefix"), + localizer.getMessage("lblWarnLimitedCard", + StringUtils.capitalize(token.getLimitedCardType().name()), getGameFormatLabel())); + case CARD_FROM_NOT_ALLOWED_SET -> + localizer.getMessage("lblErrNotAllowedCard", getGameFormatLabel()); + case CARD_FROM_INVALID_SET -> localizer.getMessage("lblErrCardEditionDate"); + case UNSUPPORTED_CARD -> localizer.getMessage("lblErrUnsupportedCard", this.currentGameType); + case UNKNOWN_CARD -> String.format("%s: %s", localizer.getMessage("lblWarningMsgPrefix"), + localizer.getMessage("lblWarnUnknownCardMsg")); + case CARD_NOT_IN_INVENTORY -> localizer.getMessage("lblWarnNotInInventory"); + default -> ""; + }; + } + + + private String getTokenFoilLabel(DeckRecognizer.Token token) { + if (!token.isCardToken()) + return ""; + final String foilMarker = "- (Foil)"; + return token.getCard().isFoil() ? foilMarker : ""; + } + + private String getGameFormatLabel() { + return String.format("\"%s\"", this.getCurrentGameFormatName()); + } } diff --git a/forge-gui/src/main/java/forge/deck/DeckProxy.java b/forge-gui/src/main/java/forge/deck/DeckProxy.java index d0c473003c5..ae90813cf6d 100644 --- a/forge-gui/src/main/java/forge/deck/DeckProxy.java +++ b/forge-gui/src/main/java/forge/deck/DeckProxy.java @@ -774,7 +774,7 @@ public class DeckProxy implements InventoryItem { for (PaperCard c : deck.getAllCardsInASinglePool().toFlatList()) { CardEdition edition = FModel.getMagicDb().getEditions().get(c.getEdition()); - if (edition == null) + if (edition == null || !edition.hasBasicLands()) continue; availableEditions.add(edition); } diff --git a/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestData.java b/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestData.java index 544a57e84db..0ed47c54c12 100644 --- a/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestData.java +++ b/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestData.java @@ -18,15 +18,11 @@ package forge.gamemodes.planarconquest; import java.io.File; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; import forge.card.CardDb; import forge.gamemodes.planarconquest.ConquestPreferences.CQPref; @@ -199,14 +195,11 @@ public final class ConquestData { } public int getAccessiblePlaneCount() { - // TODO: Java 8 stream implementation of filtering - int i = 0; - for (ConquestPlane plane : FModel.getPlanes()) { - if (!plane.isUnreachable()) { - i++; - } - } - return i; + return (int) FModel.getPlanes().stream().filter(Predicate.not(ConquestPlane::isUnreachable)).count(); + } + + public Set getUnlockedPlanes() { + return planeDataMap.values().stream().map(ConquestPlaneData::getLocation).map(ConquestLocation::getPlane).collect(Collectors.toSet()); } public void unlockPlane(ConquestPlane plane) { @@ -302,7 +295,7 @@ public final class ConquestData { } } - if (commandersUsingCard.length() > 0) { + if (!commandersUsingCard.isEmpty()) { SOptionPane.showMessageDialog(Localizer.getInstance().getMessage("lblCommandersCardCannotBeExiledByCard", CardTranslation.getTranslatedName(card.getName()), commandersUsingCard), title, SOptionPane.INFORMATION_ICON); return false; } diff --git a/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestPlane.java b/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestPlane.java index 70caed02419..dc51523487f 100644 --- a/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestPlane.java +++ b/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestPlane.java @@ -50,6 +50,7 @@ public class ConquestPlane { private FCollection commanders; private ConquestAwardPool awardPool; private ConquestEvent[] events; + private final Set editions = new HashSet<>(); private ConquestPlane(String name0, String description0, int regionSize0, boolean unreachable0) { name = name0; @@ -153,6 +154,10 @@ public class ConquestPlane { return planeCards; } + public Set getEditions() { + return editions; + } + private void ensureRegionsLoaded() { if (regions != null) { return; } @@ -193,6 +198,8 @@ public class ConquestPlane { if (edition == null) continue; + editions.add(edition); + for (EditionEntry card : edition.getObtainableCards()) { if (bannedCardSet == null || !bannedCardSet.contains(card.name())) { addCard(commonCards.getCard(card.name(), setCode)); diff --git a/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestUtil.java b/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestUtil.java index e70544d620b..1d095807c7a 100644 --- a/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestUtil.java +++ b/forge-gui/src/main/java/forge/gamemodes/planarconquest/ConquestUtil.java @@ -1,22 +1,16 @@ package forge.gamemodes.planarconquest; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import forge.card.CardRarity; -import forge.card.CardRules; -import forge.card.CardType; +import forge.card.*; import forge.card.CardType.CoreType; -import forge.card.ColorSet; -import forge.card.MagicColor; import forge.card.mana.ManaCostShard; import forge.deck.CardPool; import forge.deck.Deck; @@ -143,6 +137,25 @@ public class ConquestUtil { return pool; } + public static List getBasicLandSets(Deck currentDeck) { + ConquestData model = FModel.getConquest().getModel(); + List planes = new ArrayList<>(model.getUnlockedPlanes()); + ConquestPlane currentPlane = model.getCurrentPlane(); + //Move the current plane to the front. + if(currentPlane != null && planes.contains(currentPlane)) { + planes.remove(currentPlane); + planes.add(0, currentPlane); + } + //Move editions of cards already in the deck to the front. + Map editionStats = currentDeck.getAllCardsInASinglePool().getCardEditionStatistics(true); + List out = planes.stream() + .mapMulti((p, c) -> p.getEditions().forEach(c)) + .filter(CardEdition::hasBasicLands) + .sorted(Comparator.comparing(e -> editionStats.getOrDefault(e, 0))) + .collect(Collectors.toList()); + return out; + } + public static ConquestPlane getPlaneByName(String planeName) { for (ConquestPlane plane : FModel.getPlanes()) { if (plane.getName().equals(planeName)) { @@ -189,19 +202,16 @@ public class ConquestUtil { public static int getShardValue(CardRarity rarity, CQPref baseValuePref) { ConquestPreferences prefs = FModel.getConquestPreferences(); int baseValue = prefs.getPrefInt(baseValuePref); - switch (rarity) { - case Common: - return baseValue; - case Uncommon: - return Math.round((float)baseValue * (float)prefs.getPrefInt(CQPref.AETHER_UNCOMMON_MULTIPLIER)); - case Rare: - case Special: - return Math.round((float)baseValue * (float)prefs.getPrefInt(CQPref.AETHER_RARE_MULTIPLIER)); - case MythicRare: - return Math.round((float)baseValue * (float)prefs.getPrefInt(CQPref.AETHER_MYTHIC_MULTIPLIER)); - default: - return 0; - } + return switch (rarity) { + case Common -> baseValue; + case Uncommon -> + Math.round((float) baseValue * (float) prefs.getPrefInt(CQPref.AETHER_UNCOMMON_MULTIPLIER)); + case Rare, Special -> + Math.round((float) baseValue * (float) prefs.getPrefInt(CQPref.AETHER_RARE_MULTIPLIER)); + case MythicRare -> + Math.round((float) baseValue * (float) prefs.getPrefInt(CQPref.AETHER_MYTHIC_MULTIPLIER)); + default -> 0; + }; } public enum AEtherFilter implements IHasSkinProp { diff --git a/forge-gui/src/main/java/forge/gamemodes/quest/QuestController.java b/forge-gui/src/main/java/forge/gamemodes/quest/QuestController.java index 6cb61c47c51..05dd5907ec2 100644 --- a/forge-gui/src/main/java/forge/gamemodes/quest/QuestController.java +++ b/forge-gui/src/main/java/forge/gamemodes/quest/QuestController.java @@ -19,6 +19,7 @@ package forge.gamemodes.quest; import java.io.File; import java.util.*; +import java.util.stream.Collectors; import com.google.common.collect.Lists; import com.google.common.eventbus.Subscribe; @@ -620,17 +621,21 @@ public class QuestController { } public CardEdition getDefaultLandSet() { - List availableEditionCodes = questFormat != null ? questFormat.getAllowedSetCodes() : Lists.newArrayList(FModel.getMagicDb().getEditions().getItemNames()); - List availableEditions = new ArrayList<>(); - - for (String s : availableEditionCodes) { - availableEditions.add(FModel.getMagicDb().getEditions().get(s)); - } + List availableEditions = getAvailableLandSets(); CardEdition randomLandSet = CardEdition.Predicates.getRandomSetWithAllBasicLands(availableEditions); return randomLandSet == null ? FModel.getMagicDb().getEditions().get("ZEN") : randomLandSet; } + public List getAvailableLandSets() { + List availableEditionCodes = questFormat != null ? questFormat.getAllowedSetCodes() : Lists.newArrayList(FModel.getMagicDb().getEditions().getItemNames()); + CardEdition.Collection editions = FModel.getMagicDb().getEditions(); + return availableEditionCodes.stream() + .map(editions::get) + .filter(CardEdition::hasBasicLands) + .collect(Collectors.toList()); + } + public String getCurrentDeck() { return model.currentDeck; }