Added Commander quest mode and world

-CardPool
 Added getFilteredPool() to easily get a Predicate applied copy of a CardPool.

 -GameRules
 Minor formatting change.

 -worlds.txt
 Added Random Commander to the list.

 -DeckConstructionRules
 New enum for defining the subformat a quest is using.

 -QuestAssets
 getLife() now has a switch for modifying the life for sub-formats.

 -QuestData
 New data save version. Includes a DeckConstructionRules enum.

 -QuestDataIO
 updateSaveFile will update old saves to have a default DeckConstructionRules complying with the new QuestData save version.

 -QuestController
 Updated to include support for DeckConstructionRules and specialized duel managers

 -QuestEvent
 Now have boolean to define if this is a "random" match for the duel list. Currently only QuestEventCommanderDuelManager makes use of this feature for Commander quests.

 -QuestEventCommanderDuel
 New QuestEventDuel used in the QuestEventCommanderDuelManager which contains a DeckProxy for use in generating random commander decks.

 -QuestEventCommanderDuelManager
 New duel manager to generate duels by difficulty for a Commander quest. Currently uses random generation to generate the decks of each opponent.

 -QuestSpellShop
 Sell Extras button now has a switch for taking into account special deck construction rules such as Commander only allowing singletons.

 -QuestUtil
 Starting a game now checks for various sub-format specific changes including a switch case for which variety of registered player to use.

 -QuestUtilCards
 Starting cardpool size is now modified by a switch case for sub-formats such as Commander.

 -QuestWinLoseController
 QuestEvents marked as random matches will now award a "Random Opponent Bonus" equal to the credit base. Currently only QuestEventCommanderDuelManager creates QuestEvents marked as such.

 -QuestWorld
 Added support for the Commander quest format and world.

 -CEditorQuest
 Many changes to add support for Commander in a style that, hopefully, also paths the way for future format support.

 -CSubmenuQuestData
 Support for Commander quests.

 -VSubmenuQuestData
 Support for Commander quests.
This commit is contained in:
Jeremy Pelkala
2018-11-02 18:57:17 -04:00
parent a564e6af53
commit 6abd3c45b4
19 changed files with 561 additions and 36 deletions

View File

@@ -17,6 +17,7 @@
*/ */
package forge.deck; package forge.deck;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.StaticData; import forge.StaticData;
import forge.card.CardDb; import forge.card.CardDb;
@@ -216,4 +217,17 @@ public class CardPool extends ItemPool<PaperCard> {
} }
return sb.toString(); 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<PaperCard> predicate){
CardPool filteredPool = new CardPool();
for(PaperCard pc : this.items.keySet()){
if(predicate.apply(pc)) filteredPool.add(pc);
}
return filteredPool;
}
} }

View File

@@ -78,7 +78,8 @@ public class GameRules {
} }
public boolean hasCommander() { 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); || appliedVariants.contains(GameType.Brawl);
} }

View File

@@ -18,11 +18,20 @@
package forge.screens.deckeditor.controllers; package forge.screens.deckeditor.controllers;
import com.google.common.base.Function; 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 com.google.common.base.Supplier;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.UiCommand; 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.CardPool;
import forge.deck.Deck; import forge.deck.Deck;
import forge.deck.DeckSection; import forge.deck.DeckSection;
import forge.deck.generation.DeckGeneratorBase;
import forge.gui.GuiUtils; import forge.gui.GuiUtils;
import forge.gui.framework.DragCell; import forge.gui.framework.DragCell;
import forge.gui.framework.FScreen; import forge.gui.framework.FScreen;
@@ -35,6 +44,7 @@ import forge.itemmanager.views.ItemTableColumn;
import forge.model.FModel; import forge.model.FModel;
import forge.properties.ForgePreferences.FPref; import forge.properties.ForgePreferences.FPref;
import forge.quest.QuestController; import forge.quest.QuestController;
import forge.quest.data.DeckConstructionRules;
import forge.screens.deckeditor.AddBasicLandsDialog; import forge.screens.deckeditor.AddBasicLandsDialog;
import forge.screens.deckeditor.SEditorIO; import forge.screens.deckeditor.SEditorIO;
import forge.screens.deckeditor.views.VAllDecks; import forge.screens.deckeditor.views.VAllDecks;
@@ -48,6 +58,7 @@ import forge.util.ItemPool;
import javax.swing.*; import javax.swing.*;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.awt.print.Paper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -103,6 +114,14 @@ public final class CEditorQuest extends CDeckEditor<Deck> {
allSections.add(DeckSection.Main); allSections.add(DeckSection.Main);
allSections.add(DeckSection.Sideboard); 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; this.questData = questData0;
final CardManager catalogManager = new CardManager(cDetailPicture, false, true); final CardManager catalogManager = new CardManager(cDetailPicture, false, true);
@@ -158,6 +177,10 @@ public final class CEditorQuest extends CDeckEditor<Deck> {
@Override @Override
protected CardLimit getCardLimit() { protected CardLimit getCardLimit() {
if (FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)) { 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.Default;
} }
return CardLimit.None; //if not enforcing deck legality, don't enforce default limit return CardLimit.None; //if not enforcing deck legality, don't enforce default limit
@@ -245,16 +268,98 @@ public final class CEditorQuest extends CDeckEditor<Deck> {
public void resetTables() { public void resetTables() {
this.sectionMode = DeckSection.Main; 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 // show cards, makes this user friendly
this.getCatalogManager().setPool(cardpool); this.getCatalogManager().setPool(getRemainingCardPool());
this.getDeckManager().setPool(deck.getMain()); 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<PaperCard> 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<PaperCard> {
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 !mc.isPureGeneric() && 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<PaperCard> commanders = getDeck().getOrCreate(DeckSection.Commander).toFlatList();
List<String> colors = new ArrayList<>();
//Return early if there are no current commanders
if(commanders.size() == 0) 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");
}
return ColorSet.fromNames(colors);
}
/*
Used to make the code more readable in game terms.
*/
private Deck getDeck(){
return this.controller.getModel();
}
private ItemPool<PaperCard> getCommanderCardPool(){
Predicate<PaperCard> commanderPredicate = Predicates.compose(CardRulesPredicates.Presets.CAN_BE_COMMANDER, PaperCard.FN_GET_RULES);
return getRemainingCardPool().getFilteredPool(commanderPredicate);
} }
@Override @Override
@@ -280,14 +385,30 @@ public final class CEditorQuest extends CDeckEditor<Deck> {
} }
/** /**
* 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) { public void setEditorMode(DeckSection sectionMode) {
if (sectionMode == DeckSection.Sideboard) { //Fixes null pointer error on switching tabs while quest deck editor is open. TODO: Find source of bug possibly?
this.getDeckManager().setPool(this.controller.getModel().getOrCreate(DeckSection.Sideboard)); if(sectionMode == null) sectionMode = DeckSection.Main;
}
else { //Based on which section the editor is in, display the remaining card pool (or applicable card pool if in
this.getDeckManager().setPool(this.controller.getModel().getMain()); //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; this.sectionMode = sectionMode;

View File

@@ -10,6 +10,7 @@ import forge.model.FModel;
import forge.properties.ForgeConstants; import forge.properties.ForgeConstants;
import forge.quest.*; import forge.quest.*;
import forge.quest.StartingPoolPreferences.PoolType; import forge.quest.StartingPoolPreferences.PoolType;
import forge.quest.data.DeckConstructionRules;
import forge.quest.data.GameFormatQuest; import forge.quest.data.GameFormatQuest;
import forge.quest.data.QuestData; import forge.quest.data.QuestData;
import forge.quest.data.QuestPreferences.QPref; import forge.quest.data.QuestPreferences.QPref;
@@ -340,9 +341,16 @@ public enum CSubmenuQuestData implements ICDoc {
break; 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(); 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(); FModel.getQuest().save();
// Save in preferences. // Save in preferences.

View File

@@ -62,6 +62,7 @@ public enum VSubmenuQuestData implements IVSubmenu<CSubmenuQuestData> {
private final FRadioButton radHard = new FRadioButton("Hard"); private final FRadioButton radHard = new FRadioButton("Hard");
private final FRadioButton radExpert = new FRadioButton("Expert"); private final FRadioButton radExpert = new FRadioButton("Expert");
private final FCheckBox boxFantasy = new FCheckBox("Fantasy Mode"); 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 FLabel lblStartingWorld = new FLabel.Builder().text("Starting world:").build();
private final FComboBoxWrapper<QuestWorld> cbxStartingWorld = new FComboBoxWrapper<>(); private final FComboBoxWrapper<QuestWorld> cbxStartingWorld = new FComboBoxWrapper<>();
@@ -274,9 +275,25 @@ public enum VSubmenuQuestData implements IVSubmenu<CSubmenuQuestData> {
} }
}); });
// Fantasy box enabled by Default // Fantasy box selected by Default
boxFantasy.setSelected(true); boxFantasy.setSelected(true);
boxFantasy.setEnabled(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); boxCompleteSet.setEnabled(true);
boxAllowDuplicates.setEnabled(true); boxAllowDuplicates.setEnabled(true);
@@ -286,6 +303,7 @@ public enum VSubmenuQuestData implements IVSubmenu<CSubmenuQuestData> {
final JPanel pnlDifficultyMode = new JPanel(new MigLayout("insets 0, gap 1%, flowy")); final JPanel pnlDifficultyMode = new JPanel(new MigLayout("insets 0, gap 1%, flowy"));
pnlDifficultyMode.add(difficultyPanel, "gapright 4%"); pnlDifficultyMode.add(difficultyPanel, "gapright 4%");
pnlDifficultyMode.add(boxFantasy, "h 25px!, gapbottom 15, 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"); pnlDifficultyMode.add(lblStartingWorld, "h 25px!, hidemode 3");
cbxStartingWorld.addTo(pnlDifficultyMode, "h 27px!, w 40%, pushx, gapbottom 7"); cbxStartingWorld.addTo(pnlDifficultyMode, "h 27px!, w 40%, pushx, gapbottom 7");
pnlDifficultyMode.setOpaque(false); pnlDifficultyMode.setOpaque(false);
@@ -487,6 +505,14 @@ public enum VSubmenuQuestData implements IVSubmenu<CSubmenuQuestData> {
return boxFantasy.isSelected(); return boxFantasy.isSelected();
} }
/**
* Auth. Imakuni
* @return True if the "Commander Subformat" check box is selected.
*/
public boolean isCommander() {
return boxCommander.isSelected();
}
public boolean startWithCompleteSet() { public boolean startWithCompleteSet() {
return boxCompleteSet.isSelected(); return boxCompleteSet.isSelected();
} }

View File

@@ -1,5 +1,6 @@
Name:Main world Name:Main world
Name:Random Standard Name:Random Standard
Name:Random Commander
Name:Amonkhet|Dir:Amonkhet|Sets:AKH, HOU Name:Amonkhet|Dir:Amonkhet|Sets:AKH, HOU
Name:Jamuraa|Dir:jamuraa|Sets:5ED, ARN, MIR, VIS, WTH|Banned:Chaos Orb; Falling Star Name:Jamuraa|Dir:jamuraa|Sets:5ED, ARN, MIR, VIS, WTH|Banned:Chaos Orb; Falling Star
Name:Kamigawa|Dir:2004 Kamigawa|Sets:CHK, BOK, SOK Name:Kamigawa|Dir:2004 Kamigawa|Sets:CHK, BOK, SOK

View File

@@ -276,9 +276,10 @@ public class QuestController {
public void newGame(final String name, final int difficulty, final QuestMode mode, public void newGame(final String name, final int difficulty, final QuestMode mode,
final GameFormat formatPrizes, final boolean allowSetUnlocks, final GameFormat formatPrizes, final boolean allowSetUnlocks,
final Deck startingCards, final GameFormat formatStartingPool, 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) { if (startingCards != null) {
this.myCards.addDeck(startingCards); this.myCards.addDeck(startingCards);
@@ -435,6 +436,12 @@ public class QuestController {
QuestWorld world = getWorld(); QuestWorld world = getWorld();
String path = ForgeConstants.DEFAULT_CHALLENGES_DIR; 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 != null) {
if (world.getName().equals(QuestWorld.STANDARDWORLDNAME)) { if (world.getName().equals(QuestWorld.STANDARDWORLDNAME)) {
@@ -449,7 +456,6 @@ public class QuestController {
} }
this.duelManager = new QuestEventDuelManager(new File(path)); this.duelManager = new QuestEventDuelManager(new File(path));
} }
public HashSet<StarRating> GetRating() { public HashSet<StarRating> GetRating() {
@@ -607,4 +613,6 @@ public class QuestController {
public void setCurrentDeck(String s) { public void setCurrentDeck(String s) {
model.currentDeck = s; model.currentDeck = s;
} }
public DeckConstructionRules getDeckConstructionRules(){return model.deckConstructionRules;}
} }

View File

@@ -48,6 +48,7 @@ public abstract class QuestEvent implements IQuestEvent {
private String profile = "Default"; private String profile = "Default";
// Opponent name if different from the challenge name // Opponent name if different from the challenge name
private String opponentName = null; private String opponentName = null;
private boolean isRandomMatch = false;
public static final Function<QuestEvent, String> FN_GET_NAME = new Function<QuestEvent, String>() { public static final Function<QuestEvent, String> FN_GET_NAME = new Function<QuestEvent, String>() {
@@ -174,4 +175,7 @@ public abstract class QuestEvent implements IQuestEvent {
this.showDifficulty = showDifficulty; this.showDifficulty = showDifficulty;
} }
public boolean getIsRandomMatch(){return isRandomMatch;}
public void setIsRandomMatch(boolean b){isRandomMatch = b;}
} }

View File

@@ -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;}
}

View File

@@ -0,0 +1,205 @@
package forge.quest;
import forge.deck.*;
import forge.item.PaperCard;
import forge.model.FModel;
import forge.quest.data.QuestPreferences;
import forge.util.CollectionSuppliers;
import forge.util.MyRandom;
import forge.util.maps.EnumMapOfLists;
import forge.util.maps.MapOfLists;
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<QuestEventDuel> commanderDuels = new ArrayList<>();
/**
* Contains the expert deck lists for the commanders.
*/
private List<DeckProxy> 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<DeckProxy> 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<QuestEventDuel> getAllDuels() {
return commanderDuels;
}
/**
* Retrieve list of all possible Commander duels.
* @param difficulty Currently unused
* @return ArrayList containing all possible Commander duels.
*/
public Iterable<QuestEventDuel> 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<QuestEventDuel> generateDuels(){
final List<QuestEventDuel> 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<PaperCard> easyList = easyMain.toFlatList();
List<PaperCard> 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);
}
}

View File

@@ -344,11 +344,24 @@ public class QuestSpellShop {
List<Entry<InventoryItem, Integer>> cardsToRemove = new LinkedList<>(); List<Entry<InventoryItem, Integer>> cardsToRemove = new LinkedList<>();
for (Entry<InventoryItem, Integer> item : inventoryManager.getPool()) { for (Entry<InventoryItem, Integer> item : inventoryManager.getPool()) {
PaperCard card = (PaperCard)item.getKey(); PaperCard card = (PaperCard)item.getKey();
int numToKeep = card.getRules().getType().isBasic() ? //Number of a particular card to keep
FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_BASIC_LAND_SIZE) : FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_SIZE); 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())) { if (DeckFormat.getLimitExceptions().contains(card.getName())) {
numToKeep = FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_ANY_NUMBER_SIZE); numToKeep = FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_ANY_NUMBER_SIZE);
} }
if (numToKeep < item.getValue()) { if (numToKeep < item.getValue()) {
cardsToRemove.add(Pair.of(item.getKey(), item.getValue() - numToKeep)); cardsToRemove.add(Pair.of(item.getKey(), item.getValue() - numToKeep));
} }

View File

@@ -41,6 +41,7 @@ import forge.properties.ForgePreferences.FPref;
import forge.quest.bazaar.IQuestBazaarItem; import forge.quest.bazaar.IQuestBazaarItem;
import forge.quest.bazaar.QuestItemType; import forge.quest.bazaar.QuestItemType;
import forge.quest.bazaar.QuestPetController; import forge.quest.bazaar.QuestPetController;
import forge.quest.data.DeckConstructionRules;
import forge.quest.data.QuestAchievements; import forge.quest.data.QuestAchievements;
import forge.quest.data.QuestAssets; import forge.quest.data.QuestAssets;
import forge.util.gui.SGuiChoose; import forge.util.gui.SGuiChoose;
@@ -51,6 +52,7 @@ import org.apache.commons.lang3.tuple.ImmutablePair;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.TreeSet;
/** /**
* <p> * <p>
@@ -531,7 +533,17 @@ public class QuestUtil {
Integer lifeHuman = null; Integer lifeHuman = null;
boolean useBazaar = true; boolean useBazaar = true;
Boolean forceAnte = null; 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) { if (event instanceof QuestEventChallenge) {
final QuestEventChallenge qc = ((QuestEventChallenge) event); final QuestEventChallenge qc = ((QuestEventChallenge) event);
lifeAI = qc.getAILife(); lifeAI = qc.getAILife();
@@ -545,8 +557,9 @@ public class QuestUtil {
forceAnte = qc.isForceAnte(); forceAnte = qc.isForceAnte();
} }
final RegisteredPlayer humanStart = new RegisteredPlayer(getDeckForNewGame()); final RegisteredPlayer humanStart = getRegisteredPlayerByVariant(getDeckForNewGame());
final RegisteredPlayer aiStart = new RegisteredPlayer(event.getEventDeck());
final RegisteredPlayer aiStart = getRegisteredPlayerByVariant(event.getEventDeck());
if (lifeHuman != null) { if (lifeHuman != null) {
humanStart.setStartingLife(lifeHuman); humanStart.setStartingLife(lifeHuman);
@@ -581,17 +594,39 @@ public class QuestUtil {
rules.setGamesPerMatch(qData.getMatchLength()); rules.setGamesPerMatch(qData.getMatchLength());
rules.setManaBurn(FModel.getPreferences().getPrefBoolean(FPref.UI_MANABURN)); rules.setManaBurn(FModel.getPreferences().getPrefBoolean(FPref.UI_MANABURN));
rules.setCanCloneUseTargetsImage(FModel.getPreferences().getPrefBoolean(FPref.UI_CLONE_MODE_SOURCE)); rules.setCanCloneUseTargetsImage(FModel.getPreferences().getPrefBoolean(FPref.UI_CLONE_MODE_SOURCE));
TreeSet<GameType> variant = new TreeSet();
if(FModel.getQuest().getDeckConstructionRules() == DeckConstructionRules.Commander){
variant.add(GameType.Commander);
}
final HostedMatch hostedMatch = GuiBase.getInterface().hostMatch(); final HostedMatch hostedMatch = GuiBase.getInterface().hostMatch();
final IGuiGame gui = GuiBase.getInterface().getNewGuiGame(); final IGuiGame gui = GuiBase.getInterface().getNewGuiGame();
gui.setPlayerAvatar(aiPlayer, event); gui.setPlayerAvatar(aiPlayer, event);
FThreads.invokeInEdtNowOrLater(new Runnable(){ FThreads.invokeInEdtNowOrLater(new Runnable(){
@Override @Override
public void run() { 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() { private static Deck getDeckForNewGame() {
Deck deck = null; Deck deck = null;
if (event instanceof QuestEventChallenge) { if (event instanceof QuestEventChallenge) {
@@ -623,7 +658,7 @@ public class QuestUtil {
} }
if (FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)) { if (FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)) {
final String errorMessage = GameType.Quest.getDeckFormat().getDeckConformanceProblem(deck); final String errorMessage = getDeckConformanceProblems(deck);
if (null != errorMessage) { if (null != errorMessage) {
SOptionPane.showErrorDialog("Your deck " + errorMessage + " Please edit or choose a different deck.", "Invalid Deck"); SOptionPane.showErrorDialog("Your deck " + errorMessage + " Please edit or choose a different deck.", "Invalid Deck");
return false; return false;
@@ -633,6 +668,21 @@ public class QuestUtil {
return true; 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 /** Duplicate in DeckEditorQuestMenu and
* probably elsewhere...can streamline at some point * probably elsewhere...can streamline at some point
* (probably shouldn't be here). * (probably shouldn't be here).

View File

@@ -308,10 +308,16 @@ public final class QuestUtilCards {
* user preferences * user preferences
*/ */
public void setupNewGameCardPool(final GameFormat formatStartingPool, final int idxDifficulty, final StartingPoolPreferences userPrefs) { 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 nC = (int)(questPreferences.getPrefInt(DifficultyPrefs.STARTING_COMMONS, idxDifficulty) * variantModifier);
final int nU = questPreferences.getPrefInt(DifficultyPrefs.STARTING_UNCOMMONS, idxDifficulty); final int nU = (int)(questPreferences.getPrefInt(DifficultyPrefs.STARTING_UNCOMMONS, idxDifficulty) * variantModifier);
final int nR = questPreferences.getPrefInt(DifficultyPrefs.STARTING_RARES, idxDifficulty); final int nR = (int)(questPreferences.getPrefInt(DifficultyPrefs.STARTING_RARES, idxDifficulty) * variantModifier);
addAllCards(BoosterUtils.getQuestStarterDeck(formatStartingPool, nC, nU, nR, userPrefs)); addAllCards(BoosterUtils.getQuestStarterDeck(formatStartingPool, nC, nU, nR, userPrefs));

View File

@@ -226,6 +226,11 @@ public class QuestWinLoseController {
sb.append(StringUtils.capitalize(qEvent.getDifficulty().getTitle())); sb.append(StringUtils.capitalize(qEvent.getDifficulty().getTitle()));
sb.append(" opponent: ").append(credBase).append(" credits.\n"); 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 winMultiplier = Math.min(qData.getAchievements().getWin(), FModel.getQuestPreferences().getPrefInt(QPref.REWARDS_WINS_MULTIPLIER_MAX));
final int creditsForPreviousWins = (int) ((Double.parseDouble(FModel.getQuestPreferences() final int creditsForPreviousWins = (int) ((Double.parseDouble(FModel.getQuestPreferences()
.getPref(QPref.REWARDS_WINS_MULTIPLIER)) * winMultiplier)); .getPref(QPref.REWARDS_WINS_MULTIPLIER)) * winMultiplier));

View File

@@ -40,6 +40,7 @@ public class QuestWorld implements Comparable<QuestWorld>{
private final String dir; private final String dir;
private final GameFormatQuest format; private final GameFormatQuest format;
public static final String STANDARDWORLDNAME = "Random Standard"; public static final String STANDARDWORLDNAME = "Random Standard";
public static final String RANDOMCOMMANDERWORLDNAME = "Random Commander";
private boolean isCustom; private boolean isCustom;
@@ -129,7 +130,6 @@ public class QuestWorld implements Comparable<QuestWorld>{
/** /**
* TODO: Write javadoc for Constructor. * TODO: Write javadoc for Constructor.
* @param file0 * @param file0
* @param keySelector0
*/ */
public Reader(String file0) { public Reader(String file0) {
super(file0, QuestWorld.FN_GET_NAME); super(file0, QuestWorld.FN_GET_NAME);
@@ -194,6 +194,12 @@ public class QuestWorld implements Comparable<QuestWorld>{
FModel.getFormats().getStandard().getBannedCardNames(),false); 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); // System.out.println("Creating quest world " + useName + " (index " + useIdx + ", dir: " + useDir);
// if (useFormat != null) { System.out.println("SETS: " + sets + "\nBANNED: " + bannedCards); } // if (useFormat != null) { System.out.println("SETS: " + sets + "\nBANNED: " + bannedCards); }

View File

@@ -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
}

View File

@@ -200,7 +200,14 @@ public class QuestAssets {
* @return the life * @return the life
*/ */
public int getLife(final QuestMode mode) { 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); return (base + this.getItemLevel(QuestItemType.ELIXIR_OF_LIFE)) - this.getItemLevel(QuestItemType.POUND_FLESH);
} }

View File

@@ -42,7 +42,7 @@ import java.util.Map;
*/ */
public final class QuestData { public final class QuestData {
/** Holds the latest version of the Quest Data. */ /** 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, // This field places the version number into QD instance,
// but only when the object is created through the constructor // but only when the object is created through the constructor
@@ -70,6 +70,11 @@ public final class QuestData {
public String currentDeck = "DEFAULT"; 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 public QuestData() { //needed for XML serialization
} }
@@ -87,9 +92,11 @@ public final class QuestData {
* allow set unlocking during quest * allow set unlocking during quest
* @param startingWorld * @param startingWorld
* starting world * starting world
* @param dcr
* deck construction rules e.g. Commander
*/ */
public QuestData(String name0, int diff, QuestMode mode0, GameFormat userFormat, 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; this.name = name0;
if (userFormat != null) { if (userFormat != null) {
@@ -99,6 +106,7 @@ public final class QuestData {
this.achievements = new QuestAchievements(diff); this.achievements = new QuestAchievements(diff);
this.assets = new QuestAssets(format); this.assets = new QuestAssets(format);
this.worldId = startingWorld; this.worldId = startingWorld;
this.deckConstructionRules = dcr;
} }
/** /**

View File

@@ -223,10 +223,16 @@ public class QuestDataIO {
// Current Deck moved from preferences to quest data - it should not be global for all quests!!! // 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)); 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 // Migrate DraftTournaments to use new Tournament class
} }
final QuestAssets qS = newData.getAssets(); final QuestAssets qS = newData.getAssets();
final QuestAchievements qA = newData.getAchievements(); final QuestAchievements qA = newData.getAchievements();