diff --git a/.gitattributes b/.gitattributes index c983334e0a4..221171cc7d8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16749,6 +16749,7 @@ forge-gui/src/main/java/forge/match/input/InputSelectTargets.java -text forge-gui/src/main/java/forge/match/input/InputSynchronized.java -text forge-gui/src/main/java/forge/match/input/InputSyncronizedBase.java -text forge-gui/src/main/java/forge/match/input/package-info.java -text +forge-gui/src/main/java/forge/model/Achievement.java -text forge-gui/src/main/java/forge/model/CardBlock.java -text forge-gui/src/main/java/forge/model/CardCollections.java -text forge-gui/src/main/java/forge/model/FModel.java svneol=native#text/plain diff --git a/forge-ai/src/main/java/forge/ai/AiCardMemory.java b/forge-ai/src/main/java/forge/ai/AiCardMemory.java index 8f6ee2995c2..0ef60a2d692 100644 --- a/forge-ai/src/main/java/forge/ai/AiCardMemory.java +++ b/forge-ai/src/main/java/forge/ai/AiCardMemory.java @@ -51,7 +51,7 @@ public class AiCardMemory { */ public enum MemorySet { MANDATORY_ATTACKERS, - HELD_MANA_SOURCES, // stub, not linked to AI code yet + HELD_MANA_SOURCES, //REVEALED_CARDS // stub, not linked to AI code yet } diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 91d9d17fd65..a168744f555 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -68,6 +68,7 @@ import forge.game.replacement.ReplaceMoved; import forge.game.replacement.ReplacementEffect; import forge.game.spellability.Ability; import forge.game.spellability.AbilityManaPart; +import forge.game.spellability.AbilityStatic; import forge.game.spellability.AbilitySub; import forge.game.spellability.OptionalCost; import forge.game.spellability.Spell; @@ -740,6 +741,7 @@ public class AiController { && !ComputerUtil.castPermanentInMain1(player, sa)) { return AiPlayDecision.WaitForMain2; } + // save cards with flash for surprise blocking if (card.hasKeyword("Flash") && (player.isUnlimitedHandSize() || player.getCardsIn(ZoneType.Hand).size() <= player.getMaxHandSize()) @@ -750,6 +752,20 @@ public class AiController { && !ComputerUtil.castPermanentInMain1(player, sa)) { return AiPlayDecision.AnotherTime; } + + // don't play cards without being able to pay the upkeep for + for (String ability : card.getKeyword()) { + if (ability.startsWith("At the beginning of your upkeep, sacrifice CARDNAME unless you pay")) { + final String[] k = ability.split(" pay "); + final String costs = k[1].replaceAll("[{]", "").replaceAll("[}]", " "); + Cost cost = new Cost(costs, true); + final Ability emptyAbility = new AbilityStatic(card, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } }; + emptyAbility.setActivatingPlayer(player); + if (!ComputerUtilCost.canPayCost(emptyAbility, player)) { + return AiPlayDecision.AnotherTime; + } + } + } return canPlayFromEffectAI((SpellPermanent)sa, false, true); } else if( sa instanceof Spell ) { @@ -759,7 +775,7 @@ public class AiController { } private AiPlayDecision canPlaySpellBasic(final Card card) { - if (card.getSVar("NeedsToPlay").length() > 0) { + if (card.hasSVar("NeedsToPlay")) { final String needsToPlay = card.getSVar("NeedsToPlay"); List list = game.getCardsIn(ZoneType.Battlefield); diff --git a/forge-ai/src/main/java/forge/ai/AiProps.java b/forge-ai/src/main/java/forge/ai/AiProps.java index f32700aeefb..e90fa5a5866 100644 --- a/forge-ai/src/main/java/forge/ai/AiProps.java +++ b/forge-ai/src/main/java/forge/ai/AiProps.java @@ -29,7 +29,8 @@ public enum AiProps { /** */ DEFAULT_PLANAR_DIE_ROLL_CHANCE ("50"), /** */ MULLIGAN_THRESHOLD ("5"), /** */ PLANAR_DIE_ROLL_HESITATION_CHANCE ("10"), - CHEAT_WITH_MANA_ON_SHUFFLE ("FALSE"); /** */ + CHEAT_WITH_MANA_ON_SHUFFLE ("false"), + MOVE_EQUIPMENT_TO_BETTER_CREATURES ("always"); /** */ private final String strDefaultVal; diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java index 25853e79bac..e113c6c4b2c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -985,8 +985,8 @@ public class AttachAi extends SpellAbilityAi { // TODO AttachSource is currently set for the Source of the Spell, but // at some point can support attaching a different card - // Don't equip if already equipping - if (attachSource.getEquippingCard() != null && attachSource.getEquippingCard().getController() == aiPlayer || attachSource.hasSVar("DontEquip")) { + // Don't equip if DontEquip SVar is set + if (attachSource.hasSVar("DontEquip")) { return null; } // Don't fortify if already fortifying @@ -1017,6 +1017,35 @@ public class AttachAi extends SpellAbilityAi { Card c = attachGeneralAI(aiPlayer, sa, prefList, mandatory, attachSource, sa.getParam("AILogic")); + AiController aic = ((PlayerControllerAi)aiPlayer.getController()).getAi(); + if (c != null && attachSource.getType().contains("Equipment") + && attachSource.getEquippingCard() != null + && attachSource.getEquippingCard().getController() == aiPlayer) { + if (c.equals(attachSource.getEquippingCard())) { + // Do not equip if equipping the same card already + return null; + } + + if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("never")) { + // Do not equip other creatures if the AI profile does not allow moving equipment around + return null; + } else if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("from_useless_only")) { + // Do not equip other creatures if the AI profile only allows moving equipment from useless creatures + // and the equipped creature is still useful (not non-untapping+tapped and not set to can't attack/block) + if (!isUselessCreature(aiPlayer, attachSource.getEquippingCard())) { + return null; + } + } + + // make sure to prioritize casting spells in main 2 (creatures, other equipment, etc.) rather than moving equipment around + if (aic.getCardMemory().isMemorySetEmpty(AiCardMemory.MemorySet.HELD_MANA_SOURCES)) { + SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Attach); + if (futureSpell != null && futureSpell.getHostCard() != null) { + aic.reserveManaSourcesForMain2(futureSpell); + } + } + } + if ((c == null) && mandatory) { CardLists.shuffle(list); c = list.get(0); @@ -1086,7 +1115,7 @@ public class AttachAi extends SpellAbilityAi { } // Consider exceptional cases which break the normal evaluation rules - if (!isUsefulAttachAction(c, sa)) { + if (!isUsefulAttachAction(ai, c, sa)) { return null; } @@ -1279,7 +1308,7 @@ public class AttachAi extends SpellAbilityAi { * @param sa SpellAbility * @return true, if the action is useful (beneficial) in the current minimal context (Card vs. Attach SpellAbility) */ - private static boolean isUsefulAttachAction(Card c, SpellAbility sa) { + private static boolean isUsefulAttachAction(Player ai, Card c, SpellAbility sa) { if (c == null) { return false; } @@ -1292,17 +1321,28 @@ public class AttachAi extends SpellAbilityAi { ArrayList cardTypes = sa.getHostCard().getType(); - if (cardTypes.contains("Equipment") && c.hasKeyword("CARDNAME can't attack or block.")) { + if (cardTypes.contains("Equipment") && isUselessCreature(ai, c)) { // useless to equip a creature that can't attack or block. return false; - } else if (cardTypes.contains("Equipment") && c.isTapped() && c.hasKeyword("CARDNAME doesn't untap during your untap step.")) { - // useless to equip a creature that won't untap and is tapped. - return false; } return true; } + private static boolean isUselessCreature(Player ai, Card c) { + if (c == null) { + return true; + } + + if (c.hasKeyword("CARDNAME can't attack or block.") + || (c.hasKeyword("CARDNAME doesn't untap during your untap step.") && c.isTapped()) + || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) { + return true; + } + + return false; + } + @Override public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { return true; diff --git a/forge-gui-desktop/src/main/java/forge/GuiDesktop.java b/forge-gui-desktop/src/main/java/forge/GuiDesktop.java index 1376f2ce139..5474d257ade 100644 --- a/forge-gui-desktop/src/main/java/forge/GuiDesktop.java +++ b/forge-gui-desktop/src/main/java/forge/GuiDesktop.java @@ -32,6 +32,7 @@ import forge.error.BugReportDialog; import forge.events.UiEvent; import forge.game.GameType; import forge.game.Match; +import forge.game.card.Card; import forge.game.phase.PhaseType; import forge.game.player.IHasIcon; import forge.game.player.RegisteredPlayer; @@ -152,17 +153,30 @@ public class GuiDesktop implements IGuiBase { @Override public T showInputDialog(String message, String title, FSkinProp icon, T initialInput, T[] inputOptions) { + if (initialInput instanceof Card || (inputOptions != null && inputOptions.length > 0 && inputOptions[0] instanceof Card)) { + System.err.println("Warning: Cards passed to GUI! Printing stack trace."); + Thread.dumpStack(); + } return FOptionPane.showInputDialog(message, title, icon == null ? null : FSkin.getImage(icon), initialInput, inputOptions); } @Override public List getChoices(final String message, final int min, final int max, final Collection choices, final T selected, final Function display) { + if ((choices != null && !choices.isEmpty() && choices.iterator().next() instanceof Card) || selected instanceof Card) { + System.err.println("Warning: Cards passed to GUI! Printing stack trace."); + Thread.dumpStack(); + } return GuiChoose.getChoices(message, min, max, choices, selected, display); } @Override public List order(final String title, final String top, final int remainingObjectsMin, final int remainingObjectsMax, final List sourceChoices, final List destChoices, final CardView referenceCard, final boolean sideboardingMode) { + if ((sourceChoices != null && !sourceChoices.isEmpty() && sourceChoices.iterator().next() instanceof Card) + || (destChoices != null && !destChoices.isEmpty() && destChoices.iterator().next() instanceof Card)) { + System.err.println("Warning: Cards passed to GUI! Printing stack trace."); + Thread.dumpStack(); + } return GuiChoose.order(title, top, remainingObjectsMin, remainingObjectsMax, sourceChoices, destChoices, referenceCard, sideboardingMode); } diff --git a/forge-gui-mobile/src/forge/assets/FSkinImage.java b/forge-gui-mobile/src/forge/assets/FSkinImage.java index 80b2c1c18f5..cab822fc479 100644 --- a/forge-gui-mobile/src/forge/assets/FSkinImage.java +++ b/forge-gui-mobile/src/forge/assets/FSkinImage.java @@ -105,6 +105,11 @@ public enum FSkinImage implements FImage { ARCSON (FSkinProp.ICO_ARCSON, SourceFile.ICONS), ARCSHOVER (FSkinProp.ICO_ARCSHOVER, SourceFile.ICONS), + //Achievement Trophies + BRONZE_TROPHY (FSkinProp.IMG_BRONZE_TROPHY, SourceFile.ICONS), + SILVER_TROPHY (FSkinProp.IMG_SILVER_TROPHY, SourceFile.ICONS), + GOLD_TROPHY (FSkinProp.IMG_GOLD_TROPHY, SourceFile.ICONS), + //Quest Icons QUEST_ZEP (FSkinProp.ICO_QUEST_ZEP, SourceFile.ICONS), QUEST_GEAR (FSkinProp.ICO_QUEST_GEAR, SourceFile.ICONS), diff --git a/forge-gui-mobile/src/forge/screens/match/MatchScreen.java b/forge-gui-mobile/src/forge/screens/match/MatchScreen.java index 8f99250e544..d5e51a4f35b 100644 --- a/forge-gui-mobile/src/forge/screens/match/MatchScreen.java +++ b/forge-gui-mobile/src/forge/screens/match/MatchScreen.java @@ -170,9 +170,12 @@ public class MatchScreen extends FScreen { if (defender instanceof CardView) { TargetingOverlay.drawArrow(g, attacker, (CardView) defender); } - //connect each blocker with the attacker it's blocking - for (final CardView blocker : combat.getBlockers(attacker)) { - TargetingOverlay.drawArrow(g, blocker, attacker); + final Iterable blockers = combat.getBlockers(attacker); + if (blockers != null) { + //connect each blocker with the attacker it's blocking + for (final CardView blocker : combat.getBlockers(attacker)) { + TargetingOverlay.drawArrow(g, blocker, attacker); + } } } } diff --git a/forge-gui-mobile/src/forge/toolbox/FOptionPane.java b/forge-gui-mobile/src/forge/toolbox/FOptionPane.java index a9afac095d6..9659e4b3e83 100644 --- a/forge-gui-mobile/src/forge/toolbox/FOptionPane.java +++ b/forge-gui-mobile/src/forge/toolbox/FOptionPane.java @@ -8,7 +8,6 @@ import forge.Graphics; import forge.assets.FSkinFont; import forge.assets.FImage; import forge.assets.FSkinImage; -import forge.assets.ImageUtil; import forge.card.CardRenderer; import forge.card.CardZoom; import forge.screens.match.views.VPrompt; @@ -246,11 +245,7 @@ public class FOptionPane extends FDialog { float promptHeight = 0; if (lblIcon != null) { - float maxLabelWidth = Utils.AVG_FINGER_WIDTH * 0.8f; - float labelWidth = ImageUtil.getNearestHQSize(maxLabelWidth, lblIcon.getIcon().getWidth()); - if (labelWidth > maxLabelWidth) { - labelWidth /= 2; - } + float labelWidth = Utils.scaleX(lblIcon.getIcon().getWidth()); promptHeight = lblIcon.getIcon().getHeight() * labelWidth / lblIcon.getIcon().getWidth(); if (promptHeight > maxPromptHeight) { promptHeight = maxPromptHeight; diff --git a/forge-gui/CHANGES.txt b/forge-gui/CHANGES.txt index 6a1894b86e7..9bbc47affdf 100644 --- a/forge-gui/CHANGES.txt +++ b/forge-gui/CHANGES.txt @@ -27,6 +27,15 @@ This also applies to spells with Replicate and Multikicker to allow picking the When playing spells and abilities with the text "target opponent", if you only have one opponent, you will no longer be asked to choose the opponent to target. When triggered abilities have only one valid target, that target will now be auto-selected. +- AI improvements - +Some artificial intelligence improvements were made in this version of Forge. +The AI will now always attack with creatures that it has temporarily gained control of until end of turn in order not to miss the opportunity and thus waste the gain control spell. +The AI will no longer bounce guild lands back to hand right after playing them, which sometimes caused the AI to lock itself on land drops completely. +The AI will now try to predict if it wants to cast a spell in Main 2 and reserve some mana for it instead of aggressively pumping creatures with all available mana. +The AI will now also consider pumping a creature if it can predict that this creature will deal more damage to the opponent with its increased power, which should eliminate the senseless attacks with 0/1 creatures that do not get pumped even when they do not get blocked. +The AI will no longer waste equipment on cards that are useless (e.g. are tapped and do not normally untap, or can't attack or block anymore, etc.). +The AI can now optionally move equipment from one creature to another. For this purpose, a new AI profile variable was added: MOVE_EQUIPMENT_TO_BETTER_CREATURES. It defines whether the AI will always move equipment to better creatures if it has mana ('always'), will only move if the currently equipped creature becomes useless as defined above or because the AI loses control of the creature ('from_useless_only'), or will never move equipment around ('never'). The "Default" profile is set to only move the equipment to other creatures if the currently equipped creatures become useless for the AI, while the "Reckless" AI profile always moves equipment to better creatures when given a chance to (if it has mana and if it doesn't need to reserve mana for a future spell in Main 2). + - New Commander 2014 and Khans of Tarkir cards - We have added a branch to our SVN for the new cards that are currently being scripted. These cards are not yet available in this build of forge. Please be patient and they will soon become available. @@ -64,6 +73,38 @@ Mardu Skullhunter Heir of the Wilds Ivorytusk Fortress Sagu Mauler +Ghostfire Blade +Abzan Ascendancy +Flying Crane Technique +Temur Charger +Utter End +Clever Impersonator +End Hostilities +Ugin's Nexus +Anafenza, the Foremost +Crater's Claws +Hardened Scales +See the Unwritten +Sarkhan, the Dragonspeaker +Sorin, Solemn Visitor +Frontier Bivouac +Mystic Monastery +Nomad Outpost +Opulent Palace +Sandsteppe Citadel +Dragon's Eye Savants +Horde Ambusher +Ruthless Ripper +Watcher of the Roost +Abzan Guide +Jeering Instigator +Ankle Shanker +Jeskai Windscout +Mantis Rider +Murderous Cut +Rakshasa Deathdealer +Savage Knuckleblade +Shambling Attendants ------------ diff --git a/forge-gui/res/ai/Default.ai b/forge-gui/res/ai/Default.ai index ca186e10f73..8e38572c431 100644 --- a/forge-gui/res/ai/Default.ai +++ b/forge-gui/res/ai/Default.ai @@ -4,3 +4,4 @@ DEFAULT_MIN_TURN_TO_ROLL_PLANAR_DIE=3 DEFAULT_PLANAR_DIE_ROLL_CHANCE=50 MULLIGAN_THRESHOLD=5 PLANAR_DIE_ROLL_HESITATION_CHANCE=10 +MOVE_EQUIPMENT_TO_BETTER_CREATURES=from_useless_only diff --git a/forge-gui/res/ai/Reckless.ai b/forge-gui/res/ai/Reckless.ai index dfa63f97b38..e342edee2f7 100644 --- a/forge-gui/res/ai/Reckless.ai +++ b/forge-gui/res/ai/Reckless.ai @@ -2,5 +2,6 @@ CHEAT_WITH_MANA_ON_SHUFFLE=true DEFAULT_MAX_PLANAR_DIE_ROLLS_PER_TURN=1 DEFAULT_MIN_TURN_TO_ROLL_PLANAR_DIE=1 DEFAULT_PLANAR_DIE_ROLL_CHANCE=100 -MULLIGAN_THRESHOLD=2 +MULLIGAN_THRESHOLD=3 PLANAR_DIE_ROLL_HESITATION_CHANCE=0 +MOVE_EQUIPMENT_TO_BETTER_CREATURES=always diff --git a/forge-gui/res/cardsfolder/h/homicidal_seclusion.txt b/forge-gui/res/cardsfolder/h/homicidal_seclusion.txt index 27fe691d8f9..4c9084f29ae 100644 --- a/forge-gui/res/cardsfolder/h/homicidal_seclusion.txt +++ b/forge-gui/res/cardsfolder/h/homicidal_seclusion.txt @@ -1,8 +1,9 @@ -Name:Homicidal Seclusion -ManaCost:4 B -Types:Enchantment -S:Mode$ Continuous | Affected$ Creature.YouCtrl | AddPower$ 3 | AddToughness$ 1 | AddKeyword$ Lifelink | CheckSVar$ X | SVarCompare$ EQ1 | Description$ As long as you control exactly one creature, that creature gets +3/+1 and has lifelink. -SVar:X:Count$Valid Creature.YouCtrl -SVar:PlayMain1:TRUE -SVar:Picture:http://www.wizards.com/global/images/magic/general/homicidal_seclusion.jpg -Oracle:As long as you control exactly one creature, that creature gets +3/+1 and has lifelink. +Name:Homicidal Seclusion +ManaCost:4 B +Types:Enchantment +S:Mode$ Continuous | Affected$ Creature.YouCtrl | AddPower$ 3 | AddToughness$ 1 | AddKeyword$ Lifelink | CheckSVar$ X | SVarCompare$ EQ1 | Description$ As long as you control exactly one creature, that creature gets +3/+1 and has lifelink. +SVar:X:Count$Valid Creature.YouCtrl +SVar:RemAIDeck:True +SVar:PlayMain1:TRUE +SVar:Picture:http://www.wizards.com/global/images/magic/general/homicidal_seclusion.jpg +Oracle:As long as you control exactly one creature, that creature gets +3/+1 and has lifelink. diff --git a/forge-gui/res/skins/default/sprite_icons.png b/forge-gui/res/skins/default/sprite_icons.png index ef7dac35ef2..8c6d688e877 100644 Binary files a/forge-gui/res/skins/default/sprite_icons.png and b/forge-gui/res/skins/default/sprite_icons.png differ diff --git a/forge-gui/src/main/java/forge/assets/FSkinProp.java b/forge-gui/src/main/java/forge/assets/FSkinProp.java index c130eea8f33..ed0066148ea 100644 --- a/forge-gui/src/main/java/forge/assets/FSkinProp.java +++ b/forge-gui/src/main/java/forge/assets/FSkinProp.java @@ -231,6 +231,11 @@ public enum FSkinProp { IMG_PACK (new int[] {80, 760, 40, 40}, PropType.IMAGE), IMG_SORCERY (new int[] {160, 720, 40, 40}, PropType.IMAGE), + //achievement trophies + IMG_BRONZE_TROPHY (new int[] {0, 880, 60, 80}, PropType.IMAGE), + IMG_SILVER_TROPHY (new int[] {60, 880, 60, 80}, PropType.IMAGE), + IMG_GOLD_TROPHY (new int[] {120, 880, 60, 80}, PropType.IMAGE), + //button images IMG_BTN_START_UP (new int[] {480, 200, 160, 80}, PropType.ICON), IMG_BTN_START_OVER (new int[] {480, 280, 160, 80}, PropType.ICON), diff --git a/forge-gui/src/main/java/forge/control/FControlGameEventHandler.java b/forge-gui/src/main/java/forge/control/FControlGameEventHandler.java index 7be01868c82..ab06145dcaf 100644 --- a/forge-gui/src/main/java/forge/control/FControlGameEventHandler.java +++ b/forge-gui/src/main/java/forge/control/FControlGameEventHandler.java @@ -53,6 +53,7 @@ import forge.interfaces.IGuiBase; import forge.match.input.ButtonUtil; import forge.match.input.InputBase; import forge.model.FModel; +import forge.player.LobbyPlayerHuman; import forge.properties.ForgePreferences.FPref; import forge.util.Lang; import forge.util.gui.SGuiChoose; @@ -177,6 +178,9 @@ public class FControlGameEventHandler extends IGameEventVisitor.Base { gui.showPromptMessage(""); //clear prompt behind WinLose overlay ButtonUtil.update(gui, "", "", false, false, false); gui.finishGame(); + if (gui.getGuiPlayer() instanceof LobbyPlayerHuman) { + gameView.updateAchievements((LobbyPlayerHuman) gui.getGuiPlayer()); + } } }); return null; diff --git a/forge-gui/src/main/java/forge/model/Achievement.java b/forge-gui/src/main/java/forge/model/Achievement.java new file mode 100644 index 00000000000..b27542da8d2 --- /dev/null +++ b/forge-gui/src/main/java/forge/model/Achievement.java @@ -0,0 +1,275 @@ +package forge.model; + +import java.io.FileNotFoundException; +import java.net.MalformedURLException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import forge.assets.FSkinProp; +import forge.game.Game; +import forge.game.player.Player; +import forge.game.zone.ZoneType; +import forge.interfaces.IGuiBase; +import forge.properties.ForgeConstants; +import forge.util.XmlUtil; +import forge.util.gui.SOptionPane; + +public enum Achievement { + WinStreak("Win Streak", true, + "Win 10 games in a row.", 10, + "Win 25 games in a row.", 25, + "Win 50 games in a row.", 50, + new Evaluator() { + @Override + public int evaluate(Player player, Game game, int current) { + if (player.getOutcome().hasWon()) { + return current + 1; + } + return 0; //reset if player didn't win + } + }), + TotalWins("Total Wins", true, + "Win 100 games.", 100, + "Win 250 games.", 250, + "Win 500 games.", 500, + new Evaluator() { + @Override + public int evaluate(Player player, Game game, int current) { + if (player.getOutcome().hasWon()) { + return current + 1; + } + return current; + } + }), + Overkill("Overkill", true, + "Win game with opponent at -25 life or less.", 25, + "Win game with opponent at -50 life or less.", 50, + "Win game with opponent at -100 life or less.", 100, + new Evaluator() { + @Override + public int evaluate(Player player, Game game, int current) { + if (player.getOutcome().hasWon()) { + Player opponent = getSingleOpponent(player); + if (opponent != null && opponent.getLife() < 0) { + return -opponent.getLife(); + } + } + return 0; + } + }), + LifeToSpare("Life to Spare", true, + "Win game with 20 life more than you started with.", 20, + "Win game with 40 life more than you started with.", 40, + "Win game with 80 life more than you started with.", 80, + new Evaluator() { + @Override + public int evaluate(Player player, Game game, int current) { + if (player.getOutcome().hasWon()) { + int gainedLife = player.getLife() - player.getStartingLife(); + if (gainedLife > 0) { + return gainedLife; + } + } + return 0; + } + }), + Hellbent("Hellbent", false, + "Win game with no cards in hand.", 1, + "Win game with no cards in hand or library.", 2, + "Win game with no cards in hand, library, or graveyard.", 3, + new Evaluator() { + @Override + public int evaluate(Player player, Game game, int current) { + if (player.getOutcome().hasWon()) { + if (player.getZone(ZoneType.Hand).size() == 0) { + if (player.getZone(ZoneType.Library).size() == 0) { + if (player.getZone(ZoneType.Graveyard).size() == 0) { + return 3; + } + return 2; + } + return 1; + } + } + return 0; + } + }); + + private final String displayName, bronzeDesc, silverDesc, goldDesc; + private final int bronzeThreshold, silverThreshold, goldThreshold; + private final boolean showBest; + private final Evaluator evaluator; + private int best, current; + + private Achievement(String displayName0, boolean showBest0, + String bronzeDesc0, int bronzeThreshold0, + String silverDesc0, int silverThreshold0, + String goldDesc0, int goldThreshold0, + Evaluator evaluator0) { + displayName = displayName0; + showBest = showBest0; + bronzeDesc = bronzeDesc0; + bronzeThreshold = bronzeThreshold0; + silverDesc = silverDesc0; + silverThreshold = silverThreshold0; + goldDesc = goldDesc0; + goldThreshold = goldThreshold0; + evaluator = evaluator0; + } + + public String getDisplayName() { + return displayName; + } + public String getBronzeDesc() { + return bronzeDesc; + } + public String getSilverDesc() { + return silverDesc; + } + public String getGoldDesc() { + return goldDesc; + } + public int getBest() { + return best; + } + public boolean showBest() { + return showBest; + } + public boolean earnedGold() { + return best >= goldThreshold; + } + public boolean earnedSilver() { + return best >= silverThreshold; + } + public boolean earnedBronze() { + return best >= bronzeThreshold; + } + + public static void updateAll(IGuiBase gui, Player player) { + for (Achievement achievement : Achievement.values()) { + achievement.update(gui, player, false); + } + save(); + } + + public void update(IGuiBase gui, Player player) { + update(gui, player, true); + } + private void update(IGuiBase gui, Player player, boolean save) { + current = evaluator.evaluate(player, player.getGame(), current); + if (current > best) { + int oldBest = best; + best = current; + + String type = null; + FSkinProp image = null; + String desc = null; + if (earnedGold()) { + if (oldBest < goldThreshold) { + type = "Gold"; + image = FSkinProp.IMG_GOLD_TROPHY; + desc = goldDesc; + } + } + else if (earnedSilver()) { + if (oldBest < silverThreshold) { + type = "Silver"; + image = FSkinProp.IMG_SILVER_TROPHY; + desc = silverDesc; + } + } + else if (earnedBronze()) { + if (oldBest < bronzeThreshold) { + type = "Bronze"; + image = FSkinProp.IMG_BRONZE_TROPHY; + desc = bronzeDesc; + } + } + if (type != null) { + SOptionPane.showMessageDialog(gui, "You've earned a " + type + " trophy!\n\n" + + displayName + " - " + desc, "Achievement Earned", image); + } + } + if (save) { + save(); + } + } + + public static void load() { + try { + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + final Document document = builder.parse(ForgeConstants.ACHIEVEMENTS_FILE); + final NodeList cards = document.getElementsByTagName("a"); + for (int i = 0; i < cards.getLength(); i++) { + final Element el = (Element)cards.item(i); + final Achievement achievement = Achievement.valueOf(el.getAttribute("name")); + achievement.best = getIntAttribute(el, "best"); + achievement.current = getIntAttribute(el, "current"); + } + } + catch (FileNotFoundException e) { + //ok if file not found + } + catch (MalformedURLException e) { + //ok if file not found + } + catch (Exception e) { + e.printStackTrace(); + } + } + + private static int getIntAttribute(Element el, String name) { + String value = el.getAttribute(name); + if (value.length() > 0) { + try { + return Integer.parseInt(value); + } + catch (Exception ex) {} + } + return 0; + } + + private static void save() { + try { + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document document = builder.newDocument(); + Element root = document.createElement("achievements"); + document.appendChild(root); + + for (Achievement achievement : Achievement.values()) { + if (achievement.best > 0 || achievement.current > 0) { + Element a = document.createElement("a"); + a.setAttribute("name", achievement.name()); + a.setAttribute("best", String.valueOf(achievement.best)); + a.setAttribute("current", String.valueOf(achievement.current)); + root.appendChild(a); + } + } + XmlUtil.saveDocument(document, ForgeConstants.ACHIEVEMENTS_FILE); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + private static abstract class Evaluator { + public abstract int evaluate(Player player, Game game, int current); + + //get single opponent of player + protected Player getSingleOpponent(Player player) { + if (player.getGame().getRegisteredPlayers().size() == 2) { + for (Player p : player.getGame().getRegisteredPlayers()) { + if (p.isOpponentOf(player)) { + return p; + } + } + } + return null; + } + } +} diff --git a/forge-gui/src/main/java/forge/model/FModel.java b/forge-gui/src/main/java/forge/model/FModel.java index c4ab1c0488e..52148d106c6 100644 --- a/forge-gui/src/main/java/forge/model/FModel.java +++ b/forge-gui/src/main/java/forge/model/FModel.java @@ -155,6 +155,7 @@ public class FModel { CardPreferences.load(); DeckPreferences.load(); ItemManagerConfig.load(); + Achievement.load(); //preload AI profiles AiProfileUtil.loadAllProfiles(ForgeConstants.AI_PROFILE_DIR); diff --git a/forge-gui/src/main/java/forge/player/LobbyPlayerHuman.java b/forge-gui/src/main/java/forge/player/LobbyPlayerHuman.java index b19f46e1226..39c8e86c277 100644 --- a/forge-gui/src/main/java/forge/player/LobbyPlayerHuman.java +++ b/forge-gui/src/main/java/forge/player/LobbyPlayerHuman.java @@ -38,4 +38,8 @@ public class LobbyPlayerHuman extends LobbyPlayer implements IGameEntitiesFactor public void hear(LobbyPlayer player, String message) { gui.hear(player, message); } + + public IGuiBase getGui() { + return this.gui; + } } \ No newline at end of file diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index d155fe836bf..146e49ae7f2 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -508,6 +508,7 @@ public class PlayerControllerHuman extends PlayerController { List toBottom = null; List toTop = null; + mayLookAt.addAll(topN); if (topN.size() == 1) { if (willPutCardOnTop(topN.get(0))) { toTop = topN; @@ -517,7 +518,8 @@ public class PlayerControllerHuman extends PlayerController { } } else { - toBottom = SGuiChoose.many(getGui(), "Select cards to be put on the bottom of your library", "Cards to put on the bottom", -1, topN, null); + final List toBottomViews = SGuiChoose.many(getGui(), "Select cards to be put on the bottom of your library", "Cards to put on the bottom", -1, gameView.getCardViews(topN), null); + toBottom = gameView.getCards(toBottomViews); topN.removeAll(toBottom); if (topN.isEmpty()) { toTop = null; @@ -526,9 +528,11 @@ public class PlayerControllerHuman extends PlayerController { toTop = topN; } else { - toTop = SGuiChoose.order(getGui(), "Arrange cards to be put on top of your library", "Cards arranged", topN, null); + final List toTopViews = SGuiChoose.order(getGui(), "Arrange cards to be put on top of your library", "Cards arranged", gameView.getCardViews(topN), null); + toTop = gameView.getCards(toTopViews); } } + mayLookAt.clear(); return ImmutablePair.of(toTop, toBottom); } @@ -565,8 +569,10 @@ public class PlayerControllerHuman extends PlayerController { @Override public List chooseCardsToDiscardFrom(Player p, SpellAbility sa, List valid, int min, int max) { if (p != player) { + mayLookAt.addAll(valid); final List choices = SGuiChoose.many(getGui(), "Choose " + min + " card" + (min != 1 ? "s" : "") + " to discard", "Discarded", min, min, gameView.getCardViews(valid), null); + mayLookAt.clear(); return getCards(choices); } diff --git a/forge-gui/src/main/java/forge/properties/ForgeConstants.java b/forge-gui/src/main/java/forge/properties/ForgeConstants.java index 5a8456239b8..c055e31e5bd 100644 --- a/forge-gui/src/main/java/forge/properties/ForgeConstants.java +++ b/forge-gui/src/main/java/forge/properties/ForgeConstants.java @@ -98,6 +98,7 @@ public final class ForgeConstants { DECK_PREFS_FILE = USER_PREFS_DIR + "deck.preferences"; QUEST_PREFS_FILE = USER_PREFS_DIR + "quest.preferences"; ITEM_VIEW_PREFS_FILE = USER_PREFS_DIR + "item_view.preferences"; + ACHIEVEMENTS_FILE = USER_PREFS_DIR + "achievements.xml"; _DEFAULTS_DIR = RES_DIR + "defaults/"; NO_CARD_FILE = _DEFAULTS_DIR + "no_card.jpg"; @@ -228,6 +229,7 @@ public final class ForgeConstants { public static String DECK_PREFS_FILE; public static String QUEST_PREFS_FILE; public static String ITEM_VIEW_PREFS_FILE; + public static String ACHIEVEMENTS_FILE; // data that has defaults in the program dir but overrides/additions in the user dir private static String _DEFAULTS_DIR; diff --git a/forge-gui/src/main/java/forge/view/CombatView.java b/forge-gui/src/main/java/forge/view/CombatView.java index 03ddb8ff142..4e311efe2d0 100644 --- a/forge-gui/src/main/java/forge/view/CombatView.java +++ b/forge-gui/src/main/java/forge/view/CombatView.java @@ -50,6 +50,12 @@ public class CombatView { } return false; } + + /** + * @param attacker + * @return the blockers associated with an attacker, or {@code null} if the + * attacker is unblocked. + */ public Iterable getBlockers(final CardView attacker) { return attackersWithBlockers.get(attacker); } diff --git a/forge-gui/src/main/java/forge/view/IGameView.java b/forge-gui/src/main/java/forge/view/IGameView.java index 974573f5529..3c806ab0dec 100644 --- a/forge-gui/src/main/java/forge/view/IGameView.java +++ b/forge-gui/src/main/java/forge/view/IGameView.java @@ -10,6 +10,7 @@ import forge.game.GameOutcome; import forge.game.GameType; import forge.game.phase.PhaseType; import forge.game.player.RegisteredPlayer; +import forge.player.LobbyPlayerHuman; import forge.util.ITriggerEvent; public interface IGameView { @@ -36,6 +37,7 @@ public interface IGameView { public abstract GameOutcome.AnteResult getAnteResult(); public abstract boolean isGameOver(); + public abstract void updateAchievements(LobbyPlayerHuman player); public abstract int getPoisonCountersToLose(); diff --git a/forge-gui/src/main/java/forge/view/LocalGameView.java b/forge-gui/src/main/java/forge/view/LocalGameView.java index 2e606bedc56..73cd149dd06 100644 --- a/forge-gui/src/main/java/forge/view/LocalGameView.java +++ b/forge-gui/src/main/java/forge/view/LocalGameView.java @@ -26,7 +26,10 @@ import forge.game.player.RegisteredPlayer; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityStackInstance; import forge.game.zone.ZoneType; +import forge.model.Achievement; +import forge.player.LobbyPlayerHuman; import forge.util.ITriggerEvent; +import forge.util.ThreadUtil; public class LocalGameView implements IGameView { @@ -139,6 +142,24 @@ public class LocalGameView implements IGameView { return game.isGameOver(); } + @Override + public void updateAchievements(final LobbyPlayerHuman player) { + //update all achievements for GUI player after game finished + ThreadUtil.invokeInGameThread(new Runnable() { + @Override + public void run() { + if (game == null) { + return; + } + for (final Player p : game.getRegisteredPlayers()) { + if (p.getController().getLobbyPlayer() == player) { + Achievement.updateAll(player.getGui(), p); + } + } + } + }); + } + @Override public int getPoisonCountersToLose() { return game.getRules().getPoisonCountersToLose(); diff --git a/forge-gui/src/main/java/forge/view/PlayerView.java b/forge-gui/src/main/java/forge/view/PlayerView.java index 737ee4a08e2..b0964f83865 100644 --- a/forge-gui/src/main/java/forge/view/PlayerView.java +++ b/forge-gui/src/main/java/forge/view/PlayerView.java @@ -311,6 +311,17 @@ public class PlayerView extends GameEntityView { } } + public int getNCards(final ZoneType zone) { + switch (zone) { + case Hand: + return getnHandCards(); + case Library: + return getnLibraryCards(); + default: + return getCards(zone).size(); + } + } + /** * @return the nHandCards */