diff --git a/forge-core/src/main/java/forge/deck/CardPool.java b/forge-core/src/main/java/forge/deck/CardPool.java index 72ab8efb851..b484f31ab98 100644 --- a/forge-core/src/main/java/forge/deck/CardPool.java +++ b/forge-core/src/main/java/forge/deck/CardPool.java @@ -17,6 +17,7 @@ */ package forge.deck; +import com.google.common.base.Predicate; import com.google.common.collect.Lists; import forge.StaticData; import forge.card.CardDb; @@ -216,4 +217,17 @@ public class CardPool extends ItemPool { } return sb.toString(); } + + /** + * Applies a predicate to this CardPool's cards. + * @param predicate the Predicate to apply to this CardPool + * @return a new CardPool made from this CardPool with only the cards that agree with the provided Predicate + */ + public CardPool getFilteredPool(Predicate predicate){ + CardPool filteredPool = new CardPool(); + for(PaperCard pc : this.items.keySet()){ + if(predicate.apply(pc)) filteredPool.add(pc); + } + return filteredPool; + } } diff --git a/forge-game/src/main/java/forge/game/GameRules.java b/forge-game/src/main/java/forge/game/GameRules.java index dc7ed4cddf1..5e6876e9801 100644 --- a/forge-game/src/main/java/forge/game/GameRules.java +++ b/forge-game/src/main/java/forge/game/GameRules.java @@ -78,7 +78,8 @@ public class GameRules { } public boolean hasCommander() { - return appliedVariants.contains(GameType.Commander) || appliedVariants.contains(GameType.TinyLeaders) + return appliedVariants.contains(GameType.Commander) + || appliedVariants.contains(GameType.TinyLeaders) || appliedVariants.contains(GameType.Brawl); } diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuest.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuest.java index c33da12cda7..84d8477f088 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuest.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuest.java @@ -18,8 +18,14 @@ package forge.screens.deckeditor.controllers; import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; import com.google.common.base.Supplier; import forge.UiCommand; +import forge.card.CardRules; +import forge.card.CardRulesPredicates; +import forge.card.ColorSet; +import forge.card.mana.ManaCost; import forge.deck.CardPool; import forge.deck.Deck; import forge.deck.DeckSection; @@ -35,6 +41,7 @@ import forge.itemmanager.views.ItemTableColumn; import forge.model.FModel; import forge.properties.ForgePreferences.FPref; import forge.quest.QuestController; +import forge.quest.data.DeckConstructionRules; import forge.screens.deckeditor.AddBasicLandsDialog; import forge.screens.deckeditor.SEditorIO; import forge.screens.deckeditor.views.VAllDecks; @@ -103,6 +110,14 @@ public final class CEditorQuest extends CDeckEditor { allSections.add(DeckSection.Main); allSections.add(DeckSection.Sideboard); + //Add sub-format specific sections + switch(FModel.getQuest().getDeckConstructionRules()){ + case Default: break; + case Commander: + allSections.add(DeckSection.Commander); + break; + } + this.questData = questData0; final CardManager catalogManager = new CardManager(cDetailPicture, false, true); @@ -158,6 +173,10 @@ public final class CEditorQuest extends CDeckEditor { @Override protected CardLimit getCardLimit() { if (FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)) { + //If this is a commander quest, only allow single copies of cards + if(FModel.getQuest().getDeckConstructionRules() == DeckConstructionRules.Commander){ + return CardLimit.Singleton; + } return CardLimit.Default; } return CardLimit.None; //if not enforcing deck legality, don't enforce default limit @@ -245,16 +264,103 @@ public final class CEditorQuest extends CDeckEditor { public void resetTables() { this.sectionMode = DeckSection.Main; - final Deck deck = this.controller.getModel(); - - final CardPool cardpool = getInitialCatalog(); - // remove bottom cards that are in the deck from the card pool - cardpool.removeAll(deck.getMain()); - // remove sideboard cards from the catalog - cardpool.removeAll(deck.getOrCreate(DeckSection.Sideboard)); // show cards, makes this user friendly - this.getCatalogManager().setPool(cardpool); - this.getDeckManager().setPool(deck.getMain()); + this.getCatalogManager().setPool(getRemainingCardPool()); + this.getDeckManager().setPool(getDeck().getMain()); + } + + /*** + * Provides the pool of cards the player has available to add to his or her deck. Also manages showing available cards + * to choose from for special deck construction rules, e.g.: Commander. + * @return CardPool of cards available to add to the player's deck. + */ + private CardPool getRemainingCardPool(){ + final CardPool cardpool = getInitialCatalog(); + + // remove bottom cards that are in the deck from the card pool + cardpool.removeAll(getDeck().getMain()); + + // remove sideboard cards from the catalog + cardpool.removeAll(getDeck().getOrCreate(DeckSection.Sideboard)); + + switch(FModel.getQuest().getDeckConstructionRules()){ + case Default: break; + case Commander: + //remove this deck's currently selected commander(s) from the catalog + cardpool.removeAll(getDeck().getOrCreate(DeckSection.Commander)); + + //TODO: Only thin if deck conformance is being applied + if(getDeck().getOrCreate(DeckSection.Commander).toFlatList().size() > 0) { + Predicate identityPredicate = new MatchCommanderColorIdentity(getDeckColorIdentity()); + CardPool filteredPool = cardpool.getFilteredPool(identityPredicate); + + return filteredPool; + } + break; + } + + return cardpool; + } + + /** + * Predicate that filters out based on a color identity provided upon instantiation. Used to filter the card + * list when a commander is chosen so the user can more easily see what cards are available for his or her deck + * and avoid making additions that are not legal. + */ + public static class MatchCommanderColorIdentity implements Predicate { + private final ColorSet allowedColor; + + public MatchCommanderColorIdentity(ColorSet color) { + allowedColor = color; + } + + @Override + public boolean apply(PaperCard subject) { + CardRules cr = subject.getRules(); + ManaCost mc = cr.getManaCost(); + return allowedColor.containsAllColorsFrom(cr.getColorIdentity().getColor()); + } + } + + /** + * Compiles the color identity of the loaded deck based on the commanders. + * @return A ColorSet containing the color identity of the currently loaded deck. + */ + public ColorSet getDeckColorIdentity(){ + + List commanders = getDeck().getOrCreate(DeckSection.Commander).toFlatList(); + List colors = new ArrayList<>(); + + //Return early if there are no current commanders + if(commanders.size() == 0){ + colors.add("c"); + return ColorSet.fromNames(colors); + } + + //For each commander,add each color of its color identity if not already added + for(PaperCard pc : commanders){ + if(!colors.contains("w") && pc.getRules().getColorIdentity().hasWhite()) colors.add("w"); + if(!colors.contains("u") && pc.getRules().getColorIdentity().hasBlue()) colors.add("u"); + if(!colors.contains("b") && pc.getRules().getColorIdentity().hasBlack()) colors.add("b"); + if(!colors.contains("r") && pc.getRules().getColorIdentity().hasRed()) colors.add("r"); + if(!colors.contains("g") && pc.getRules().getColorIdentity().hasGreen()) colors.add("g"); + } + + colors.add("c"); + + return ColorSet.fromNames(colors); + } + + /* + Used to make the code more readable in game terms. + */ + private Deck getDeck(){ + return this.controller.getModel(); + } + + private ItemPool getCommanderCardPool(){ + Predicate commanderPredicate = Predicates.compose(CardRulesPredicates.Presets.CAN_BE_COMMANDER, PaperCard.FN_GET_RULES); + return getRemainingCardPool().getFilteredPool(commanderPredicate); } @Override @@ -280,14 +386,30 @@ public final class CEditorQuest extends CDeckEditor { } /** - * Switch between the main deck and the sideboard editor. + * Switch between the main deck and the sideboard/Command Zone editor. */ public void setEditorMode(DeckSection sectionMode) { - if (sectionMode == DeckSection.Sideboard) { - this.getDeckManager().setPool(this.controller.getModel().getOrCreate(DeckSection.Sideboard)); - } - else { - this.getDeckManager().setPool(this.controller.getModel().getMain()); + //Fixes null pointer error on switching tabs while quest deck editor is open. TODO: Find source of bug possibly? + if(sectionMode == null) sectionMode = DeckSection.Main; + + //Based on which section the editor is in, display the remaining card pool (or applicable card pool if in + //Commander) and the current section's cards + switch(sectionMode){ + case Main : + this.getCatalogManager().setup(ItemManagerConfig.CARD_CATALOG); + this.getCatalogManager().setPool(getRemainingCardPool()); + this.getDeckManager().setPool(this.controller.getModel().getMain()); + break; + case Sideboard : + this.getCatalogManager().setup(ItemManagerConfig.CARD_CATALOG); + this.getCatalogManager().setPool(getRemainingCardPool()); + this.getDeckManager().setPool(getDeck().getOrCreate(DeckSection.Sideboard)); + break; + case Commander : + this.getCatalogManager().setup(ItemManagerConfig.COMMANDER_POOL); + this.getCatalogManager().setPool(getCommanderCardPool()); + this.getDeckManager().setPool(getDeck().getOrCreate(DeckSection.Commander)); + break; } this.sectionMode = sectionMode; diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/quest/CSubmenuQuestData.java b/forge-gui-desktop/src/main/java/forge/screens/home/quest/CSubmenuQuestData.java index 131805b738b..8aa76293b55 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/quest/CSubmenuQuestData.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/quest/CSubmenuQuestData.java @@ -10,6 +10,7 @@ import forge.model.FModel; import forge.properties.ForgeConstants; import forge.quest.*; import forge.quest.StartingPoolPreferences.PoolType; +import forge.quest.data.DeckConstructionRules; import forge.quest.data.GameFormatQuest; import forge.quest.data.QuestData; import forge.quest.data.QuestPreferences.QPref; @@ -340,9 +341,16 @@ public enum CSubmenuQuestData implements ICDoc { break; } + //Apply the appropriate deck construction rules for this quest + DeckConstructionRules dcr = DeckConstructionRules.Default; + + if(VSubmenuQuestData.SINGLETON_INSTANCE.isCommander()){ + dcr = DeckConstructionRules.Commander; + } + final QuestController qc = FModel.getQuest(); - qc.newGame(questName, difficulty, mode, fmtPrizes, view.isUnlockSetsAllowed(), dckStartPool, fmtStartPool, view.getStartingWorldName(), userPrefs); + qc.newGame(questName, difficulty, mode, fmtPrizes, view.isUnlockSetsAllowed(), dckStartPool, fmtStartPool, view.getStartingWorldName(), userPrefs, dcr); FModel.getQuest().save(); // Save in preferences. diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/quest/VSubmenuQuestData.java b/forge-gui-desktop/src/main/java/forge/screens/home/quest/VSubmenuQuestData.java index 9eee9e58ca3..70e284faf3d 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/quest/VSubmenuQuestData.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/quest/VSubmenuQuestData.java @@ -62,6 +62,7 @@ public enum VSubmenuQuestData implements IVSubmenu { private final FRadioButton radHard = new FRadioButton("Hard"); private final FRadioButton radExpert = new FRadioButton("Expert"); private final FCheckBox boxFantasy = new FCheckBox("Fantasy Mode"); + private final FCheckBox boxCommander = new FCheckBox("Commander Subformat"); private final FLabel lblStartingWorld = new FLabel.Builder().text("Starting world:").build(); private final FComboBoxWrapper cbxStartingWorld = new FComboBoxWrapper<>(); @@ -274,9 +275,25 @@ public enum VSubmenuQuestData implements IVSubmenu { } }); - // Fantasy box enabled by Default + // Fantasy box selected by Default boxFantasy.setSelected(true); boxFantasy.setEnabled(true); + + // Commander box unselected by Default + boxCommander.setSelected(false); + boxCommander.setEnabled(true); + + boxCommander.addActionListener( + new ActionListener(){ + public void actionPerformed(ActionEvent e){ + if(!isCommander()) return; //do nothing if unselecting Commander Subformat + //Otherwise, set the starting world to Random Commander + cbxStartingWorld.setSelectedItem(FModel.getWorlds().get("Random Commander")); + } + } + + ); + boxCompleteSet.setEnabled(true); boxAllowDuplicates.setEnabled(true); @@ -286,6 +303,7 @@ public enum VSubmenuQuestData implements IVSubmenu { final JPanel pnlDifficultyMode = new JPanel(new MigLayout("insets 0, gap 1%, flowy")); pnlDifficultyMode.add(difficultyPanel, "gapright 4%"); pnlDifficultyMode.add(boxFantasy, "h 25px!, gapbottom 15, gapright 4%"); + pnlDifficultyMode.add(boxCommander, "h 25px!, gapbottom 15, gapright 4%"); pnlDifficultyMode.add(lblStartingWorld, "h 25px!, hidemode 3"); cbxStartingWorld.addTo(pnlDifficultyMode, "h 27px!, w 40%, pushx, gapbottom 7"); pnlDifficultyMode.setOpaque(false); @@ -487,6 +505,14 @@ public enum VSubmenuQuestData implements IVSubmenu { return boxFantasy.isSelected(); } + /** + * Auth. Imakuni + * @return True if the "Commander Subformat" check box is selected. + */ + public boolean isCommander() { + return boxCommander.isSelected(); + } + public boolean startWithCompleteSet() { return boxCompleteSet.isSelected(); } diff --git a/forge-gui-mobile/src/forge/screens/quest/NewQuestScreen.java b/forge-gui-mobile/src/forge/screens/quest/NewQuestScreen.java index 856a6a11508..fc8594e652c 100644 --- a/forge-gui-mobile/src/forge/screens/quest/NewQuestScreen.java +++ b/forge-gui-mobile/src/forge/screens/quest/NewQuestScreen.java @@ -20,6 +20,7 @@ import forge.model.FModel; import forge.properties.ForgeConstants; import forge.quest.*; import forge.quest.StartingPoolPreferences.PoolType; +import forge.quest.data.DeckConstructionRules; import forge.quest.data.GameFormatQuest; import forge.quest.data.QuestPreferences.QPref; import forge.screens.FScreen; @@ -632,7 +633,11 @@ public class NewQuestScreen extends FScreen { final StartingPoolPreferences userPrefs = new StartingPoolPreferences(getPoolType(), getPreferredColors(), cbIncludeArtifacts.isSelected(), startWithCompleteSet(), allowDuplicateCards(), numberOfBoostersField.getValue()); QuestController qc = FModel.getQuest(); - qc.newGame(questName, getSelectedDifficulty(), mode, fmtPrizes, isUnlockSetsAllowed(), dckStartPool, fmtStartPool, getStartingWorldName(), userPrefs); + + //DeckConstructionRules are only used for the desktop's commander quest mode + DeckConstructionRules dcr = DeckConstructionRules.Default; + + qc.newGame(questName, getSelectedDifficulty(), mode, fmtPrizes, isUnlockSetsAllowed(), dckStartPool, fmtStartPool, getStartingWorldName(), userPrefs, dcr); qc.save(); // Save in preferences. diff --git a/forge-gui/res/quest/world/worlds.txt b/forge-gui/res/quest/world/worlds.txt index b08931cde59..5d99427d535 100644 --- a/forge-gui/res/quest/world/worlds.txt +++ b/forge-gui/res/quest/world/worlds.txt @@ -1,5 +1,6 @@ Name:Main world Name:Random Standard +Name:Random Commander Name:Amonkhet|Dir:Amonkhet|Sets:AKH, HOU Name:Jamuraa|Dir:jamuraa|Sets:5ED, ARN, MIR, VIS, WTH|Banned:Chaos Orb; Falling Star Name:Kamigawa|Dir:2004 Kamigawa|Sets:CHK, BOK, SOK diff --git a/forge-gui/src/main/java/forge/quest/QuestController.java b/forge-gui/src/main/java/forge/quest/QuestController.java index 0d306f4431a..5366f7200bc 100644 --- a/forge-gui/src/main/java/forge/quest/QuestController.java +++ b/forge-gui/src/main/java/forge/quest/QuestController.java @@ -276,9 +276,10 @@ public class QuestController { public void newGame(final String name, final int difficulty, final QuestMode mode, final GameFormat formatPrizes, final boolean allowSetUnlocks, final Deck startingCards, final GameFormat formatStartingPool, - final String startingWorld, final StartingPoolPreferences userPrefs) { + final String startingWorld, final StartingPoolPreferences userPrefs, + DeckConstructionRules dcr) { - this.load(new QuestData(name, difficulty, mode, formatPrizes, allowSetUnlocks, startingWorld)); // pass awards and unlocks here + this.load(new QuestData(name, difficulty, mode, formatPrizes, allowSetUnlocks, startingWorld, dcr)); // pass awards and unlocks here if (startingCards != null) { this.myCards.addDeck(startingCards); @@ -435,6 +436,12 @@ public class QuestController { QuestWorld world = getWorld(); String path = ForgeConstants.DEFAULT_CHALLENGES_DIR; + //Use a variant specialized duel manager if this is a variant quest + switch(FModel.getQuest().getDeckConstructionRules()){ + case Default: break; + case Commander: this.duelManager = new QuestEventCommanderDuelManager(); return; + } + if (world != null) { if (world.getName().equals(QuestWorld.STANDARDWORLDNAME)) { @@ -449,7 +456,6 @@ public class QuestController { } this.duelManager = new QuestEventDuelManager(new File(path)); - } public HashSet GetRating() { @@ -607,4 +613,6 @@ public class QuestController { public void setCurrentDeck(String s) { model.currentDeck = s; } + + public DeckConstructionRules getDeckConstructionRules(){return model.deckConstructionRules;} } diff --git a/forge-gui/src/main/java/forge/quest/QuestEvent.java b/forge-gui/src/main/java/forge/quest/QuestEvent.java index 00a140c80e2..2a9bf6c1b63 100644 --- a/forge-gui/src/main/java/forge/quest/QuestEvent.java +++ b/forge-gui/src/main/java/forge/quest/QuestEvent.java @@ -48,6 +48,7 @@ public abstract class QuestEvent implements IQuestEvent { private String profile = "Default"; // Opponent name if different from the challenge name private String opponentName = null; + private boolean isRandomMatch = false; public static final Function FN_GET_NAME = new Function() { @@ -174,4 +175,7 @@ public abstract class QuestEvent implements IQuestEvent { this.showDifficulty = showDifficulty; } + public boolean getIsRandomMatch(){return isRandomMatch;} + + public void setIsRandomMatch(boolean b){isRandomMatch = b;} } diff --git a/forge-gui/src/main/java/forge/quest/QuestEventCommanderDuel.java b/forge-gui/src/main/java/forge/quest/QuestEventCommanderDuel.java new file mode 100644 index 00000000000..1d7924a04d7 --- /dev/null +++ b/forge-gui/src/main/java/forge/quest/QuestEventCommanderDuel.java @@ -0,0 +1,19 @@ +package forge.quest; + +import forge.deck.DeckProxy; + +/** + * A QuestEventDuel with a CommanderDeckGenerator used exclusively within QuestEventCommanderDuelManager for the + * creation of randomly generated Commander decks in a Commander variant quest. + * Auth. Imakuni & Forge + */ +public class QuestEventCommanderDuel extends QuestEventDuel{ + /** + * The CommanderDeckGenerator for this duel. + */ + private DeckProxy deckProxy; + + public DeckProxy getDeckProxy() {return deckProxy;} + + public void setDeckProxy(DeckProxy dp) {deckProxy = dp;} +} diff --git a/forge-gui/src/main/java/forge/quest/QuestEventCommanderDuelManager.java b/forge-gui/src/main/java/forge/quest/QuestEventCommanderDuelManager.java new file mode 100644 index 00000000000..c9fbaa3457c --- /dev/null +++ b/forge-gui/src/main/java/forge/quest/QuestEventCommanderDuelManager.java @@ -0,0 +1,202 @@ +package forge.quest; + +import forge.deck.*; +import forge.item.PaperCard; +import forge.model.FModel; +import forge.quest.data.QuestPreferences; +import forge.util.MyRandom; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Manages the creation of random Commander duels for a Commander variant quest. Random generation is handled via + * the CommanderDeckGenerator class. + * Auth. Forge & Imakuni#8015 + */ +public class QuestEventCommanderDuelManager implements QuestEventDuelManagerInterface { + /** + * The list of all possible Commander variant duels. + */ + private ArrayList commanderDuels = new ArrayList<>(); + + /** + * Contains the expert deck lists for the commanders. + */ + private List expertCommanderDecks; + + /** + * Immediately calls assembleDuels() to setup the commanderDuels variable. + */ + public QuestEventCommanderDuelManager(){ + assembleDuels(); + } + + /** + * Assembles the list of all possible Commander duels via CommanderDeckGenerator. Should be done within constructor. + */ + private void assembleDuels(){ + //isCardGen = true seemed to make slightly more difficult decks based purely on experience with a very small sample size. + //Gotta work on this more, its making pretty average decks after further testing. + expertCommanderDecks = CommanderDeckGenerator.getCommanderDecks(DeckFormat.Commander, true, true); + + List generatedDuels = CommanderDeckGenerator.getCommanderDecks(DeckFormat.Commander, true, false); + + for(DeckProxy dp : generatedDuels){ + QuestEventCommanderDuel duel = new QuestEventCommanderDuel(); + + duel.setDescription("Randomly generated " + dp.getName() + " commander deck."); + duel.setName(dp.getName()); + duel.setTitle(dp.getName()); + duel.setOpponentName(dp.getName()); + duel.setDifficulty(QuestEventDifficulty.EASY); + duel.setDeckProxy(dp); + + //Setting a blank deck avoids a null pointer exception. The deck is generated in generateDuels() to avoid long load times. + duel.setEventDeck(new Deck()); + + commanderDuels.add(duel); + } + } + + /** + * Retrieve list of all possible Commander duels. + * @return ArrayList containing all possible Commander duels. + */ + public Iterable getAllDuels() { + return commanderDuels; + } + + /** + * Retrieve list of all possible Commander duels. + * @param difficulty Currently unused + * @return ArrayList containing all possible Commander duels. + */ + public Iterable getDuels(QuestEventDifficulty difficulty){ + return commanderDuels; + } + + /** + * Composes an ArrayList containing 4 QuestEventDuels composed with Commander variant decks. One duel will have its + * title replaced as Random. + * @return ArrayList of QuestEventDuels containing 4 duels. + */ + public List generateDuels(){ + final List duelOpponents = new ArrayList<>(); + + //While there are less than 4 duels chosen + while(duelOpponents.size() < 4){ + //Get a random duel from the possible duels list + QuestEventCommanderDuel duel = (QuestEventCommanderDuel)commanderDuels.get(((int) (commanderDuels.size() * MyRandom.getRandom().nextDouble()))); + + //If the chosen duels list already contains this duel, get a different duel to prevent duplicate duels + if(duelOpponents.contains(duel)) continue; + + //Add the randomly chosen duel to the duel list + duelOpponents.add(duel); + + //Here the actual deck for this commander is generated by calling .getDeck() on the saved DeckProxy + duel.setEventDeck(duel.getDeckProxy().getDeck()); + + //Modify deck for difficulty + modifyDuelForDifficulty(duel); + + + } + + //Modify the stats of the final duel to hide the opponent, creating a "random" duel. + //We make a copy of the final duel and overwrite it in the duelOpponents to avoid changing the variables in + //the original duel, which gets reused. + QuestEventCommanderDuel duel = (QuestEventCommanderDuel)duelOpponents.get(duelOpponents.size() - 1); + QuestEventCommanderDuel randomDuel = new QuestEventCommanderDuel(); + + randomDuel.setName(duel.getName()); + randomDuel.setOpponentName(duel.getName()); + randomDuel.setDeckProxy(duel.getDeckProxy()); + randomDuel.setTitle("Random Opponent"); + randomDuel.setShowDifficulty(false); + randomDuel.setDescription("Fight a random generated commander opponent."); + randomDuel.setIsRandomMatch(true); + randomDuel.setEventDeck(duel.getEventDeck()); + + //Replace the final duel with this newly modified "random" duel + duelOpponents.set(duelOpponents.size()-1, randomDuel); + + return duelOpponents; + } + + /** + * Retrieves the expert level deck generation of a deck with the same commander as the provided DeckProxy. + * @param dp The easy generation commander deck + * @return The same commander's expert generation DeckProxy + */ + private Deck getExpertGenDeck(DeckProxy dp){ + for(QuestEventDuel qed : commanderDuels){ + QuestEventCommanderDuel cmdQED = (QuestEventCommanderDuel)qed; + if(cmdQED.getDeckProxy().getName().equals(dp.getName())){ + return cmdQED.getDeckProxy().getDeck(); + } + } + return null; + } + + /** + * Modifies a given duel by replacing a percentage of the deck with random cards from the more difficult generated version + * of the same commander's deck. Medium replaces 30%, Hard replaces 60%, Expert replaces 100%. + * @param duel The QuestEventCommanderDuel to modify + */ + private void modifyDuelForDifficulty(QuestEventCommanderDuel duel){ + final QuestPreferences questPreferences = FModel.getQuestPreferences(); + final int index = FModel.getQuest().getAchievements().getDifficulty(); + final int numberOfWins = FModel.getQuest().getAchievements().getWin(); + Deck expertDeck = getExpertGenDeck(duel.getDeckProxy()); + + int difficultyReplacementPercent = 0; + + //Note: The code is ordered to make the least number of comparisons I could think of at the time for speed reasons. + //In reality, it shouldn't really make much difference, but why not? + if (numberOfWins >= questPreferences.getPrefInt(QuestPreferences.DifficultyPrefs.WINS_EXPERTAI, index)) { + //At expert, the deck is replaced with the entire expert deck, and we can return immediately + duel.setEventDeck(expertDeck); + duel.setDifficulty(QuestEventDifficulty.EXPERT); + return; + } + + if (numberOfWins >= questPreferences.getPrefInt(QuestPreferences.DifficultyPrefs.WINS_MEDIUMAI, index)) { + difficultyReplacementPercent += 30; + duel.setDifficulty(QuestEventDifficulty.MEDIUM); + } else return; //return early here since it would be an easy opponent with no changes + + if (numberOfWins >= questPreferences.getPrefInt(QuestPreferences.DifficultyPrefs.WINS_HARDAI, index)) { + difficultyReplacementPercent += 30; + duel.setDifficulty(QuestEventDifficulty.HARD); + } + + CardPool easyMain = duel.getEventDeck().getMain(); + CardPool expertMain = expertDeck.getMain(); + + List easyList = easyMain.toFlatList(); + List expertList = expertMain.toFlatList(); + + //Replace cards in the easy deck with cards from the expert deck up to the difficulty replacement percent + for(int i = 0; i < difficultyReplacementPercent; i++){ + if(!easyMain.contains(expertList.get(i))) { //ensure that the card being copied over isn't already in the deck + easyMain.remove(easyList.get(i)); + easyMain.add(expertList.get(i)); + } + else{ + expertList.remove(expertList.get(i)); + i--; + if(expertList.size() == 0) break; //break if there are no more cards to copy over + } + } + } + + /** + * Randomizes the list of Commander Duels. + */ + public void randomizeOpponents(){ + Collections.shuffle(commanderDuels); + } +} diff --git a/forge-gui/src/main/java/forge/quest/QuestSpellShop.java b/forge-gui/src/main/java/forge/quest/QuestSpellShop.java index e9196e417df..96eae768963 100644 --- a/forge-gui/src/main/java/forge/quest/QuestSpellShop.java +++ b/forge-gui/src/main/java/forge/quest/QuestSpellShop.java @@ -344,11 +344,24 @@ public class QuestSpellShop { List> cardsToRemove = new LinkedList<>(); for (Entry item : inventoryManager.getPool()) { PaperCard card = (PaperCard)item.getKey(); - int numToKeep = card.getRules().getType().isBasic() ? - FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_BASIC_LAND_SIZE) : FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_SIZE); + //Number of a particular card to keep + int numToKeep = 4; + + if(card.getRules().getType().isBasic()){ + numToKeep = FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_BASIC_LAND_SIZE); + } else{ + //Choose card limit restrictions based on deck construction rules, e.g.: Commander allows only singletons + switch(FModel.getQuest().getDeckConstructionRules()){ + case Default: numToKeep = FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_SIZE); break; + case Commander: numToKeep = 1; + } + } + + //If this card has an exception to the card limit, e.g.: Relentless Rats, get the quest preference if (DeckFormat.getLimitExceptions().contains(card.getName())) { numToKeep = FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_ANY_NUMBER_SIZE); } + if (numToKeep < item.getValue()) { cardsToRemove.add(Pair.of(item.getKey(), item.getValue() - numToKeep)); } diff --git a/forge-gui/src/main/java/forge/quest/QuestUtil.java b/forge-gui/src/main/java/forge/quest/QuestUtil.java index c2fe796ef7b..7ad6b6cd335 100644 --- a/forge-gui/src/main/java/forge/quest/QuestUtil.java +++ b/forge-gui/src/main/java/forge/quest/QuestUtil.java @@ -41,6 +41,7 @@ import forge.properties.ForgePreferences.FPref; import forge.quest.bazaar.IQuestBazaarItem; import forge.quest.bazaar.QuestItemType; import forge.quest.bazaar.QuestPetController; +import forge.quest.data.DeckConstructionRules; import forge.quest.data.QuestAchievements; import forge.quest.data.QuestAssets; import forge.util.gui.SGuiChoose; @@ -51,6 +52,7 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; +import java.util.TreeSet; /** *

@@ -531,7 +533,17 @@ public class QuestUtil { Integer lifeHuman = null; boolean useBazaar = true; Boolean forceAnte = null; - int lifeAI = 20; + + //Generate a life modifier based on this quest's variant as held in the Quest Controller's DeckConstructionRules + int variantLifeModifier = 0; + + switch(FModel.getQuest().getDeckConstructionRules()){ + case Default: break; + case Commander: variantLifeModifier = 20; break; + } + + int lifeAI = 20 + variantLifeModifier; + if (event instanceof QuestEventChallenge) { final QuestEventChallenge qc = ((QuestEventChallenge) event); lifeAI = qc.getAILife(); @@ -545,8 +557,9 @@ public class QuestUtil { forceAnte = qc.isForceAnte(); } - final RegisteredPlayer humanStart = new RegisteredPlayer(getDeckForNewGame()); - final RegisteredPlayer aiStart = new RegisteredPlayer(event.getEventDeck()); + final RegisteredPlayer humanStart = getRegisteredPlayerByVariant(getDeckForNewGame()); + + final RegisteredPlayer aiStart = getRegisteredPlayerByVariant(event.getEventDeck()); if (lifeHuman != null) { humanStart.setStartingLife(lifeHuman); @@ -581,17 +594,39 @@ public class QuestUtil { rules.setGamesPerMatch(qData.getMatchLength()); rules.setManaBurn(FModel.getPreferences().getPrefBoolean(FPref.UI_MANABURN)); rules.setCanCloneUseTargetsImage(FModel.getPreferences().getPrefBoolean(FPref.UI_CLONE_MODE_SOURCE)); + + TreeSet variant = new TreeSet(); + if(FModel.getQuest().getDeckConstructionRules() == DeckConstructionRules.Commander){ + variant.add(GameType.Commander); + } + final HostedMatch hostedMatch = GuiBase.getInterface().hostMatch(); final IGuiGame gui = GuiBase.getInterface().getNewGuiGame(); gui.setPlayerAvatar(aiPlayer, event); FThreads.invokeInEdtNowOrLater(new Runnable(){ @Override public void run() { - hostedMatch.startMatch(rules, null, starter, ImmutableMap.of(humanStart, gui)); + hostedMatch.startMatch(rules, variant, starter, ImmutableMap.of(humanStart, gui)); } }); } + /** + * Uses the appropriate RegisteredPlayer command for generating a RegisteredPlayer based on this quest's variant as + * held by the QuestController's DeckConstructionRules. + * @param deck The deck to generate the RegisteredPlayer with + * @return A newly made RegisteredPlayer specific to the quest's variant + */ + private static RegisteredPlayer getRegisteredPlayerByVariant(Deck deck){ + switch (FModel.getQuest().getDeckConstructionRules()) { + case Default: + return new RegisteredPlayer(deck); + case Commander: + return RegisteredPlayer.forCommander(deck); + } + return null; + } + private static Deck getDeckForNewGame() { Deck deck = null; if (event instanceof QuestEventChallenge) { @@ -623,7 +658,7 @@ public class QuestUtil { } if (FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)) { - final String errorMessage = GameType.Quest.getDeckFormat().getDeckConformanceProblem(deck); + final String errorMessage = getDeckConformanceProblems(deck); if (null != errorMessage) { SOptionPane.showErrorDialog("Your deck " + errorMessage + " Please edit or choose a different deck.", "Invalid Deck"); return false; @@ -633,6 +668,21 @@ public class QuestUtil { return true; } + public static String getDeckConformanceProblems(Deck deck){ + String errorMessage = GameType.Quest.getDeckFormat().getDeckConformanceProblem(deck);; + + if(errorMessage != null) return errorMessage; //return immediately if the deck does not conform to quest requirements + + //Check for all applicable deck construction rules per this quests's saved DeckConstructionRules enum + switch(FModel.getQuest().getDeckConstructionRules()){ + case Commander: + errorMessage = GameType.Commander.getDeckFormat().getDeckConformanceProblem(deck); + break; + } + + return errorMessage; + } + /** Duplicate in DeckEditorQuestMenu and * probably elsewhere...can streamline at some point * (probably shouldn't be here). diff --git a/forge-gui/src/main/java/forge/quest/QuestUtilCards.java b/forge-gui/src/main/java/forge/quest/QuestUtilCards.java index c8d77d863ad..54a4887ba8a 100644 --- a/forge-gui/src/main/java/forge/quest/QuestUtilCards.java +++ b/forge-gui/src/main/java/forge/quest/QuestUtilCards.java @@ -308,10 +308,16 @@ public final class QuestUtilCards { * user preferences */ public void setupNewGameCardPool(final GameFormat formatStartingPool, final int idxDifficulty, final StartingPoolPreferences userPrefs) { + //Add additional cards to the starter card pool based on variant if applicable + double variantModifier = 0; + switch(FModel.getQuest().getDeckConstructionRules()){ + case Default: break; + case Commander: variantModifier = 2; break; + } - final int nC = questPreferences.getPrefInt(DifficultyPrefs.STARTING_COMMONS, idxDifficulty); - final int nU = questPreferences.getPrefInt(DifficultyPrefs.STARTING_UNCOMMONS, idxDifficulty); - final int nR = questPreferences.getPrefInt(DifficultyPrefs.STARTING_RARES, idxDifficulty); + final int nC = (int)(questPreferences.getPrefInt(DifficultyPrefs.STARTING_COMMONS, idxDifficulty) * variantModifier); + final int nU = (int)(questPreferences.getPrefInt(DifficultyPrefs.STARTING_UNCOMMONS, idxDifficulty) * variantModifier); + final int nR = (int)(questPreferences.getPrefInt(DifficultyPrefs.STARTING_RARES, idxDifficulty) * variantModifier); addAllCards(BoosterUtils.getQuestStarterDeck(formatStartingPool, nC, nU, nR, userPrefs)); diff --git a/forge-gui/src/main/java/forge/quest/QuestWinLoseController.java b/forge-gui/src/main/java/forge/quest/QuestWinLoseController.java index 2e93e7d4fec..265ea61bb58 100644 --- a/forge-gui/src/main/java/forge/quest/QuestWinLoseController.java +++ b/forge-gui/src/main/java/forge/quest/QuestWinLoseController.java @@ -226,6 +226,11 @@ public class QuestWinLoseController { sb.append(StringUtils.capitalize(qEvent.getDifficulty().getTitle())); sb.append(" opponent: ").append(credBase).append(" credits.\n"); + if(qEvent.getIsRandomMatch()){ + sb.append("Random Opponent Bonus: " + credBase + " credit" + (credBase > 1 ? "s." : ".") + "\n"); + credBase += credBase; + } + final int winMultiplier = Math.min(qData.getAchievements().getWin(), FModel.getQuestPreferences().getPrefInt(QPref.REWARDS_WINS_MULTIPLIER_MAX)); final int creditsForPreviousWins = (int) ((Double.parseDouble(FModel.getQuestPreferences() .getPref(QPref.REWARDS_WINS_MULTIPLIER)) * winMultiplier)); diff --git a/forge-gui/src/main/java/forge/quest/QuestWorld.java b/forge-gui/src/main/java/forge/quest/QuestWorld.java index e20dcf98ce8..4c77b0ec515 100644 --- a/forge-gui/src/main/java/forge/quest/QuestWorld.java +++ b/forge-gui/src/main/java/forge/quest/QuestWorld.java @@ -40,6 +40,7 @@ public class QuestWorld implements Comparable{ private final String dir; private final GameFormatQuest format; public static final String STANDARDWORLDNAME = "Random Standard"; + public static final String RANDOMCOMMANDERWORLDNAME = "Random Commander"; private boolean isCustom; @@ -129,7 +130,6 @@ public class QuestWorld implements Comparable{ /** * TODO: Write javadoc for Constructor. * @param file0 - * @param keySelector0 */ public Reader(String file0) { super(file0, QuestWorld.FN_GET_NAME); @@ -194,6 +194,12 @@ public class QuestWorld implements Comparable{ FModel.getFormats().getStandard().getBannedCardNames(),false); } + if (useName.equalsIgnoreCase(QuestWorld.RANDOMCOMMANDERWORLDNAME)){ + useFormat = new GameFormatQuest(QuestWorld.RANDOMCOMMANDERWORLDNAME, + FModel.getFormats().getFormat("Commander").getAllowedSetCodes(), + FModel.getFormats().getFormat("Commander").getBannedCardNames(),false); + } + // System.out.println("Creating quest world " + useName + " (index " + useIdx + ", dir: " + useDir); // if (useFormat != null) { System.out.println("SETS: " + sets + "\nBANNED: " + bannedCards); } diff --git a/forge-gui/src/main/java/forge/quest/data/DeckConstructionRules.java b/forge-gui/src/main/java/forge/quest/data/DeckConstructionRules.java new file mode 100644 index 00000000000..3744beea09d --- /dev/null +++ b/forge-gui/src/main/java/forge/quest/data/DeckConstructionRules.java @@ -0,0 +1,17 @@ +package forge.quest.data; + +/** + * Used to clarify which subformat a quest is using e.g. Commander. + * Auth. Imakuni + */ +public enum DeckConstructionRules { + /** + * Typically has no effect on Quest gameplay. + */ + Default, + + /** + * Commander ruleset. 99 card deck, no copies other than basic lands, commander(s) in Command zone + */ + Commander +} diff --git a/forge-gui/src/main/java/forge/quest/data/QuestAssets.java b/forge-gui/src/main/java/forge/quest/data/QuestAssets.java index 616c49d937b..8f7b644641b 100644 --- a/forge-gui/src/main/java/forge/quest/data/QuestAssets.java +++ b/forge-gui/src/main/java/forge/quest/data/QuestAssets.java @@ -200,7 +200,14 @@ public class QuestAssets { * @return the life */ public int getLife(final QuestMode mode) { - final int base = mode.equals(QuestMode.Fantasy) ? 15 : 20; + int base = mode.equals(QuestMode.Fantasy) ? 15 : 20; + + //Modify life for the quest's sub-format, e.g.: Commander adds 20 + switch(FModel.getQuest().getDeckConstructionRules()){ + case Default: break; + case Commander: base += 20; + } + return (base + this.getItemLevel(QuestItemType.ELIXIR_OF_LIFE)) - this.getItemLevel(QuestItemType.POUND_FLESH); } diff --git a/forge-gui/src/main/java/forge/quest/data/QuestData.java b/forge-gui/src/main/java/forge/quest/data/QuestData.java index e6a75adef7d..41574634113 100644 --- a/forge-gui/src/main/java/forge/quest/data/QuestData.java +++ b/forge-gui/src/main/java/forge/quest/data/QuestData.java @@ -42,7 +42,7 @@ import java.util.Map; */ public final class QuestData { /** Holds the latest version of the Quest Data. */ - public static final int CURRENT_VERSION_NUMBER = 12; + public static final int CURRENT_VERSION_NUMBER = 13; // This field places the version number into QD instance, // but only when the object is created through the constructor @@ -70,6 +70,11 @@ public final class QuestData { public String currentDeck = "DEFAULT"; + /** + * Holds the subformat for this quest. Defaults to DeckConstructionRules.Default. + */ + public DeckConstructionRules deckConstructionRules = DeckConstructionRules.Default; + public QuestData() { //needed for XML serialization } @@ -87,9 +92,11 @@ public final class QuestData { * allow set unlocking during quest * @param startingWorld * starting world + * @param dcr + * deck construction rules e.g. Commander */ public QuestData(String name0, int diff, QuestMode mode0, GameFormat userFormat, - boolean allowSetUnlocks, final String startingWorld) { + boolean allowSetUnlocks, final String startingWorld, DeckConstructionRules dcr) { this.name = name0; if (userFormat != null) { @@ -99,6 +106,7 @@ public final class QuestData { this.achievements = new QuestAchievements(diff); this.assets = new QuestAssets(format); this.worldId = startingWorld; + this.deckConstructionRules = dcr; } /** diff --git a/forge-gui/src/main/java/forge/quest/io/QuestDataIO.java b/forge-gui/src/main/java/forge/quest/io/QuestDataIO.java index de680502e37..cd232367df9 100644 --- a/forge-gui/src/main/java/forge/quest/io/QuestDataIO.java +++ b/forge-gui/src/main/java/forge/quest/io/QuestDataIO.java @@ -223,10 +223,16 @@ public class QuestDataIO { // Current Deck moved from preferences to quest data - it should not be global for all quests!!! QuestDataIO.setFinalField(QuestData.class, "currentDeck", newData, FModel.getQuestPreferences().getPref(QPref.CURRENT_DECK)); } - if (saveVersion < 13) { + if(saveVersion < 13){ + //Update for quest DeckConstructionRules + //Add a DeckConstructionRules set to Default. + QuestDataIO.setFinalField(QuestData.class, "deckConstructionRules", newData, DeckConstructionRules.Default); + } + if (saveVersion < 14) { // Migrate DraftTournaments to use new Tournament class } + final QuestAssets qS = newData.getAssets(); final QuestAchievements qA = newData.getAchievements();