From 1ea2378d480aefa2847ee3ee22ded3d995330b51 Mon Sep 17 00:00:00 2001 From: Chris H Date: Sat, 28 Feb 2026 16:56:52 -0500 Subject: [PATCH] Allow for quick building decks for limited UI --- forge-core/src/main/java/forge/deck/Deck.java | 25 +++++ .../deckeditor/ColorSelectionDialog.java | 66 +++++++++++ .../controllers/CEditorLimited.java | 103 ++++++++++++++++++ .../deckeditor/views/VCurrentDeck.java | 14 ++- forge-gui/res/languages/en-US.properties | 6 +- .../forge/gamemodes/limited/DeckColors.java | 2 +- .../gamemodes/limited/LimitedDeckBuilder.java | 31 ++++-- 7 files changed, 234 insertions(+), 13 deletions(-) create mode 100644 forge-gui-desktop/src/main/java/forge/screens/deckeditor/ColorSelectionDialog.java diff --git a/forge-core/src/main/java/forge/deck/Deck.java b/forge-core/src/main/java/forge/deck/Deck.java index 63e2051f076..15b0eb1cdeb 100644 --- a/forge-core/src/main/java/forge/deck/Deck.java +++ b/forge-core/src/main/java/forge/deck/Deck.java @@ -697,6 +697,31 @@ public boolean equals(final Object o) { return false; } + /** + * Replace this deck's contents with those from another deck. + * All sections and metadata are copied. + * @param other the deck to copy from + */ + public void copyFrom(Deck other) { + // Clear all current sections + for (DeckSection section : DeckSection.values()) { + CardPool pool = this.get(section); + if (pool != null) { + pool.clear(); + } + } + // Copy all sections from other + for (Entry entry : other.parts.entrySet()) { + this.getOrCreate(entry.getKey()).addAll(entry.getValue()); + } + // Copy metadata + this.setName(other.getName()); + this.setAiHints(StringUtils.join(other.aiHints, " | ")); + this.setDraftNotes(other.draftNotes); + this.tags.clear(); + this.tags.addAll(other.tags); + } + public static int getAverageCMC(Deck deck) { int totalCMC = 0; int totalCount = 0; diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/ColorSelectionDialog.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/ColorSelectionDialog.java new file mode 100644 index 00000000000..be77ddc1643 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/ColorSelectionDialog.java @@ -0,0 +1,66 @@ +package forge.screens.deckeditor; + +import forge.card.ColorSet; +import forge.card.MagicColor; +import forge.toolbox.FCheckBox; +import forge.util.Localizer; + +import javax.swing.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class ColorSelectionDialog extends JDialog { + private final List colorBoxes = new ArrayList<>(); + private ColorSet selectedColors = ColorSet.fromMask(0x1F); // All colors by default + private boolean confirmed = false; + + public ColorSelectionDialog(Window owner, ColorSet defaultSelected) { + super(owner, Localizer.getInstance().getMessage("lblChooseColors"), ModalityType.APPLICATION_MODAL); + setLayout(new BorderLayout()); + getContentPane().setBackground(Color.WHITE); // Set dialog background + JPanel panel = new JPanel(new GridLayout(0, 1)); + panel.setBackground(Color.WHITE); // Set panel background + String[] colorNames = {"White", "Blue", "Black", "Red", "Green"}; + Color[] fgColors = {Color.BLACK, Color.BLACK, Color.BLACK, Color.BLACK, Color.BLACK}; + byte[] colorMasks = {forge.card.MagicColor.WHITE, forge.card.MagicColor.BLUE, forge.card.MagicColor.BLACK, forge.card.MagicColor.RED, forge.card.MagicColor.GREEN}; + for (int i = 0; i < colorNames.length; i++) { + boolean selected = defaultSelected == null || defaultSelected.hasAnyColor(colorMasks[i]); + FCheckBox box = new FCheckBox(colorNames[i], selected); + box.setForeground(fgColors[i]); // Set label text color to black + box.setBackground(Color.WHITE); // Set checkbox background + colorBoxes.add(box); + panel.add(box); + } + add(panel, BorderLayout.CENTER); + JPanel btnPanel = new JPanel(); + btnPanel.setBackground(Color.WHITE); // Set button panel background + JButton ok = new JButton("OK"); + JButton cancel = new JButton("Cancel"); + btnPanel.add(ok); + btnPanel.add(cancel); + add(btnPanel, BorderLayout.SOUTH); + ok.addActionListener(e -> { + confirmed = true; + selectedColors = ColorSet.fromMask( + (colorBoxes.get(0).isSelected() ? MagicColor.WHITE : 0) | + (colorBoxes.get(1).isSelected() ? MagicColor.BLUE : 0) | + (colorBoxes.get(2).isSelected() ? MagicColor.BLACK : 0) | + (colorBoxes.get(3).isSelected() ? MagicColor.RED : 0) | + (colorBoxes.get(4).isSelected() ? MagicColor.GREEN : 0) + ); + setVisible(false); + }); + cancel.addActionListener(e -> setVisible(false)); + pack(); + setLocationRelativeTo(owner); + } + + public boolean isConfirmed() { + return confirmed; + } + + public ColorSet getSelectedColors() { + return selectedColors; + } +} 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 45855fe6387..a5717995289 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 @@ -17,19 +17,24 @@ */ package forge.screens.deckeditor.controllers; +import java.awt.*; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.function.Supplier; import forge.card.CardEdition; +import forge.card.ColorSet; import forge.deck.CardPool; import forge.deck.Deck; import forge.deck.DeckGroup; import forge.deck.DeckSection; import forge.game.GameType; +import forge.gamemodes.limited.DeckColors; +import forge.gamemodes.limited.LimitedDeckBuilder; import forge.gui.UiCommand; import forge.gui.framework.DragCell; import forge.gui.framework.FScreen; @@ -38,6 +43,7 @@ import forge.itemmanager.ItemManagerConfig; import forge.model.FModel; import forge.screens.deckeditor.AddBasicLandsDialog; +import forge.screens.deckeditor.ColorSelectionDialog; import forge.screens.deckeditor.SEditorIO; import forge.screens.deckeditor.views.VAllDecks; import forge.screens.deckeditor.views.VBrawlDecks; @@ -52,6 +58,8 @@ import forge.toolbox.FComboBox; import forge.util.storage.IStorage; +import javax.swing.*; + /** * Child controller for limited deck editor UI. * @@ -114,6 +122,8 @@ public CEditorLimited(final IStorage deckMap0, final FScreen screen0, DeckSection ds = (DeckSection)cb.getSelectedItem(); setEditorMode(ds); }); + + VCurrentDeck.SINGLETON_INSTANCE.getBtnAutoBuildLimited().setCommand(() -> onAutoBuildLimitedDeck()); } //========== Overridden from ACEditorBase @@ -287,4 +297,97 @@ public void resetUIChanges() { tinyLeadersDecksParent.addDoc(VTinyLeadersDecks.SINGLETON_INSTANCE); } } + + private ColorSet getMostCommonColors() { + // Gather all cards from sideboard and main deck (the pool) + List pool = new ArrayList<>(); + pool.addAll(getHumanDeck().getOrCreate(DeckSection.Sideboard).toFlatList()); + pool.addAll(getHumanDeck().getMain().toFlatList()); + int[] colorCounts = new int[5]; // WUBRG order + for (PaperCard card : pool) { + ColorSet cs = card.getRules().getColor(); + if (cs.hasWhite()) colorCounts[0]++; + if (cs.hasBlue()) colorCounts[1]++; + if (cs.hasBlack()) colorCounts[2]++; + if (cs.hasRed()) colorCounts[3]++; + if (cs.hasGreen()) colorCounts[4]++; + } + // Find the two most common colors + int first = -1, second = -1; + for (int i = 0; i < 5; i++) { + if (first == -1 || colorCounts[i] > colorCounts[first]) { + second = first; + first = i; + } else if (second == -1 || colorCounts[i] > colorCounts[second]) { + second = i; + } + } + byte[] colorMasks = {forge.card.MagicColor.WHITE, forge.card.MagicColor.BLUE, forge.card.MagicColor.BLACK, forge.card.MagicColor.RED, forge.card.MagicColor.GREEN}; + int mask = 0; + if (first != -1 && colorCounts[first] > 0) mask |= colorMasks[first]; + if (second != -1 && colorCounts[second] > 0) mask |= colorMasks[second]; + if (mask == 0) mask = 0x1F; // fallback: all colors + return ColorSet.fromMask(mask); + } + + private void onAutoBuildLimitedDeck() { + // Show dialog to ask user for colors + Window window = SwingUtilities.getWindowAncestor(VCurrentDeck.SINGLETON_INSTANCE.getPnlHeader()); + ColorSet defaultColors = getMostCommonColors(); + ColorSelectionDialog dialog = new ColorSelectionDialog(window, defaultColors); + dialog.setVisible(true); + if (!dialog.isConfirmed()) { + return; + } + ColorSet chosenColors = dialog.getSelectedColors(); + // Build deck using LimitedDeckBuilder with forHuman=true + List pool = new ArrayList<>(); + // Gather all cards from sideboard and main deck (the pool) + pool.addAll(getHumanDeck().getOrCreate(DeckSection.Sideboard).toFlatList()); + pool.addAll(getHumanDeck().getMain().toFlatList()); + DeckColors deckColors = new DeckColors(); + java.util.List colorBytes = new ArrayList<>(); + for (forge.card.MagicColor.Color c : chosenColors.getOrderedColors()) { + colorBytes.add(c.getColorMask()); + } + deckColors.setColorsByList(colorBytes); + LimitedDeckBuilder builder = new LimitedDeckBuilder(pool, deckColors, true); + Deck newDeck = builder.buildDeck(); + + // Move cards via UI methods + CardPool currentMain = getHumanDeck().getMain(); + CardPool generatedMain = newDeck.getMain(); + + // 1. Remove cards from Main that are not in generatedMain (move to sideboard) + List> toRemove = new ArrayList<>(); + for (Entry entry : currentMain) { + int inGenerated = generatedMain.count(entry.getKey()); + int inCurrent = entry.getValue(); + if (inGenerated < inCurrent) { + toRemove.add(Map.entry(entry.getKey(), inCurrent - inGenerated)); + } + } + for (Entry entry : toRemove) { + List> single = List.of(entry); + onRemoveItems(single, false); // move from main to sideboard + } + + // 2. Add cards to Main that are in generatedMain but not enough in currentMain (move from sideboard) + List> toAdd = new ArrayList<>(); + for (Entry entry : generatedMain) { + int inCurrent = currentMain.count(entry.getKey()); + int inGenerated = entry.getValue(); + if (inGenerated > inCurrent) { + toAdd.add(Map.entry(entry.getKey(), inGenerated - inCurrent)); + } + } + for (Entry entry : toAdd) { + List> single = List.of(entry); + onAddItems(single, false); // move from sideboard to main + } + + // UI will update via onAddItems/onRemoveItems + resetTables(); + getDeckController().notifyModelChanged(); + } } diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCurrentDeck.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCurrentDeck.java index 12cd0d0ccda..38c1c87925b 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCurrentDeck.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCurrentDeck.java @@ -79,6 +79,12 @@ public enum VCurrentDeck implements IVDoc { .tooltip(localizer.getMessage("ttImportDeck")) .opaque(true).hoverable(true).build(); + private final FLabel btnAutoBuildLimited = new FLabel.Builder() + .fontSize(14) + .text(localizer.getMessage("lblAutoBuildLimited")) + .tooltip("Auto-build a limited deck for a human player") + .opaque(true).hoverable(true).build(); + private final FTextField txfTitle = new FTextField.Builder().ghostText("[" + localizer.getMessage("lblNewDeck") +"]").build(); private final JPanel pnlHeader = new JPanel(); @@ -104,6 +110,7 @@ public enum VCurrentDeck implements IVDoc { pnlHeader.add(btnSaveAs, "w 26px!, h 26px!"); pnlHeader.add(btnPrintProxies, "w 26px!, h 26px!"); pnlHeader.add(btnImport, "w 61px!, h 26px!"); + pnlHeader.add(btnAutoBuildLimited, "w 120px!, h 26px!"); // Add new button near import } //========== Overridden from IVDoc @@ -197,7 +204,7 @@ public FLabel getBtnNew() { return btnNew; } - /** @return {@link forge.gui.toolbar.FTextField} */ + /** @return {@link forge.toolbox.FTextField} */ public FTextField getTxfTitle() { return txfTitle; } @@ -214,4 +221,9 @@ public JPanel getPnlHeader() { public FLabel getBtnImport() { return btnImport; } + + /** @return {@link javax.swing.JLabel} */ + public FLabel getBtnAutoBuildLimited() { + return btnAutoBuildLimited; + } } diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index eebff573a9b..8eced95f044 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -3131,7 +3131,7 @@ lblRevealFaceDownCards=Revealing face-down cards from lblLearnALesson=Learn a Lesson lblSpeed=SPEED: {0} lblMaxSpeed=SPEED: MAX! -lblCrank=CRANK! — {0} +lblCrank=CRANK! ? {0} #QuestPreferences.java lblWildOpponentNumberError=Wild Opponents can only be 0 to 3 #GauntletWinLose.java @@ -3373,4 +3373,6 @@ lblDataMigrationMsg=Data Migration completed!\nPlease check your Inventory and E #AdventureDeckEditor.java lblRemoveUnsupportedCard=Remove unsupported card lblRemoveAllUnsupportedCards=Unsupported cards have been removed from your inventory. -lbldisableCrackedItems=Disable the possibility of your items breaking after losing a boss fight. \ No newline at end of file +lbldisableCrackedItems=Disable the possibility of your items breaking after losing a boss fight. +lblAutoBuildLimited=Quick Build +lblChooseColors=Choose Colors \ No newline at end of file diff --git a/forge-gui/src/main/java/forge/gamemodes/limited/DeckColors.java b/forge-gui/src/main/java/forge/gamemodes/limited/DeckColors.java index 638f774e41b..3f20233d665 100644 --- a/forge-gui/src/main/java/forge/gamemodes/limited/DeckColors.java +++ b/forge-gui/src/main/java/forge/gamemodes/limited/DeckColors.java @@ -30,7 +30,7 @@ public class DeckColors { public int MAX_COLORS = 2; - DeckColors() {} + public DeckColors() {} DeckColors(int max_col) { // If we want to draft decks that are more than 2 colors, we can change the MAX_COLORS value here. diff --git a/forge-gui/src/main/java/forge/gamemodes/limited/LimitedDeckBuilder.java b/forge-gui/src/main/java/forge/gamemodes/limited/LimitedDeckBuilder.java index b4b2ec11444..da404ad07df 100644 --- a/forge-gui/src/main/java/forge/gamemodes/limited/LimitedDeckBuilder.java +++ b/forge-gui/src/main/java/forge/gamemodes/limited/LimitedDeckBuilder.java @@ -54,6 +54,7 @@ protected final float getSpellPercentage() { protected final List setsWithBasicLands = new ArrayList<>(); protected List rankedColorList; protected final List draftedConspiracies; + protected final boolean forHuman; // Views for aiPlayable private Iterable onColorCreatures; @@ -69,18 +70,26 @@ protected final float getSpellPercentage() { * Cards to build the deck from. * @param pClrs * Chosen colors. + * @param forHuman + * True if building for a human player, false for AI. */ - public LimitedDeckBuilder(final List dList, final DeckColors pClrs) { + public LimitedDeckBuilder(final List dList, final DeckColors pClrs, final boolean forHuman) { super(FModel.getMagicDb().getCommonCards(), DeckFormat.Limited); this.availableList = dList; this.deckColors = pClrs; this.colors = pClrs.getChosenColors(); + this.forHuman = forHuman; - // remove Unplayables - this.aiPlayables = availableList.stream() + if (forHuman) { + // For humans, all cards are playables + this.aiPlayables = new ArrayList<>(availableList); + } else { + // remove Unplayables for AI + this.aiPlayables = availableList.stream() .filter(PaperCardPredicates.fromRules(CardRulesPredicates.IS_KEPT_IN_AI_LIMITED_DECKS)) .collect(Collectors.toList()); - this.availableList.removeAll(aiPlayables); + this.availableList.removeAll(aiPlayables); + } // keep Conspiracies in a separate list this.draftedConspiracies = aiPlayables.stream() @@ -92,13 +101,17 @@ public LimitedDeckBuilder(final List dList, final DeckColors pClrs) { } /** - * Constructor. - * - * @param list - * Cards to build the deck from. + * Constructor for backward compatibility (AI by default). + */ + public LimitedDeckBuilder(final List dList, final DeckColors pClrs) { + this(dList, pClrs, false); + } + + /** + * Constructor for backward compatibility (AI by default). */ public LimitedDeckBuilder(final List list) { - this(list, new DeckColors()); + this(list, new DeckColors(), false); } @Override