Repository: demilich1/metastone
Branch: master
Commit: bab2c6a14370
Files: 2009
Total size: 2.0 MB
Directory structure:
gitextract_a_mhvwr6/
├── .gitignore
├── LICENSE
├── README.md
├── app/
│ ├── build.fxbuild
│ ├── build.gradle
│ ├── javafx.plugin
│ ├── lib/
│ │ ├── controlsfx-8.40.10-20151003.010657-492.jar
│ │ └── nitty-gritty-mvc.jar
│ ├── manifest.json
│ └── src/
│ ├── deploy/
│ │ └── package/
│ │ └── windows/
│ │ └── Metastone.iss
│ └── main/
│ ├── java/
│ │ └── net/
│ │ └── demilich/
│ │ └── metastone/
│ │ ├── ApplicationFacade.java
│ │ ├── ApplicationStartupCommand.java
│ │ ├── DevCardTools.java
│ │ ├── MetaStone.java
│ │ ├── PlayGameCommand.java
│ │ ├── gui/
│ │ │ ├── DigitFactory.java
│ │ │ ├── DigitTemplate.java
│ │ │ ├── IconFactory.java
│ │ │ ├── autoupdate/
│ │ │ │ ├── AutoUpdateMediator.java
│ │ │ │ └── CheckForUpdateCommand.java
│ │ │ ├── battleofdecks/
│ │ │ │ ├── BattleBatchResult.java
│ │ │ │ ├── BattleBatchResultToken.java
│ │ │ │ ├── BattleConfig.java
│ │ │ │ ├── BattleDeckResult.java
│ │ │ │ ├── BattleOfDecksConfigView.java
│ │ │ │ ├── BattleOfDecksMediator.java
│ │ │ │ ├── BattleOfDecksResultView.java
│ │ │ │ ├── BattleResult.java
│ │ │ │ └── StartBattleOfDecksCommand.java
│ │ │ ├── cards/
│ │ │ │ ├── CardProxy.java
│ │ │ │ ├── CardToken.java
│ │ │ │ ├── CardTokenFactory.java
│ │ │ │ ├── CardTooltip.java
│ │ │ │ └── HandCard.java
│ │ │ ├── common/
│ │ │ │ ├── BehaviourStringConverter.java
│ │ │ │ ├── CardSetStringConverter.java
│ │ │ │ ├── ComboBoxKeyHandler.java
│ │ │ │ ├── DeckFormatStringConverter.java
│ │ │ │ ├── DeckStringConverter.java
│ │ │ │ ├── HeroStringConverter.java
│ │ │ │ ├── IntegerTextField.java
│ │ │ │ └── RestrictedTextField.java
│ │ │ ├── deckbuilder/
│ │ │ │ ├── AddCardToDeckCommand.java
│ │ │ │ ├── CardEntry.java
│ │ │ │ ├── CardEntryFactory.java
│ │ │ │ ├── CardFilter.java
│ │ │ │ ├── CardFilterView.java
│ │ │ │ ├── CardListView.java
│ │ │ │ ├── CardView.java
│ │ │ │ ├── ChangeDeckNameCommand.java
│ │ │ │ ├── ChooseClassView.java
│ │ │ │ ├── DeckBuilderMediator.java
│ │ │ │ ├── DeckBuilderView.java
│ │ │ │ ├── DeckEntry.java
│ │ │ │ ├── DeckFormatProxy.java
│ │ │ │ ├── DeckInfoView.java
│ │ │ │ ├── DeckListView.java
│ │ │ │ ├── DeckNameView.java
│ │ │ │ ├── DeckProxy.java
│ │ │ │ ├── DeleteDeckCommand.java
│ │ │ │ ├── FillDeckWithRandomCardsCommand.java
│ │ │ │ ├── FilterCardsCommand.java
│ │ │ │ ├── ImportDeckCommand.java
│ │ │ │ ├── LoadDeckFormatsCommand.java
│ │ │ │ ├── LoadDecksCommand.java
│ │ │ │ ├── RemoveCardFromDeckCommand.java
│ │ │ │ ├── SaveDeckCommand.java
│ │ │ │ ├── SetActiveDeckCommand.java
│ │ │ │ ├── importer/
│ │ │ │ │ ├── HearthHeadImporter.java
│ │ │ │ │ ├── HearthPwnImporter.java
│ │ │ │ │ ├── IDeckImporter.java
│ │ │ │ │ ├── IcyVeinsImporter.java
│ │ │ │ │ ├── ImporterFactory.java
│ │ │ │ │ └── TempostormImporter.java
│ │ │ │ └── metadeck/
│ │ │ │ ├── AddDeckToMetaDeckCommand.java
│ │ │ │ ├── MetaDeckListView.java
│ │ │ │ ├── MetaDeckView.java
│ │ │ │ └── RemoveDeckFromMetaDeckCommand.java
│ │ │ ├── dialog/
│ │ │ │ ├── DialogMediator.java
│ │ │ │ ├── DialogNotification.java
│ │ │ │ ├── DialogResult.java
│ │ │ │ ├── DialogType.java
│ │ │ │ ├── IDialogListener.java
│ │ │ │ ├── ModalDialog.java
│ │ │ │ └── UserDialog.java
│ │ │ ├── gameconfig/
│ │ │ │ └── PlayerConfigView.java
│ │ │ ├── main/
│ │ │ │ └── ApplicationMediator.java
│ │ │ ├── mainmenu/
│ │ │ │ ├── MainMenuMediator.java
│ │ │ │ └── MainMenuView.java
│ │ │ ├── playmode/
│ │ │ │ ├── GameBoardView.java
│ │ │ │ ├── GameContextVisualizable.java
│ │ │ │ ├── GameToken.java
│ │ │ │ ├── HeroToken.java
│ │ │ │ ├── HumanActionPromptView.java
│ │ │ │ ├── HumanMulliganView.java
│ │ │ │ ├── LoadingBoardView.java
│ │ │ │ ├── PlayModeMediator.java
│ │ │ │ ├── PlayModeView.java
│ │ │ │ ├── StartGameCommand.java
│ │ │ │ ├── SummonToken.java
│ │ │ │ ├── animation/
│ │ │ │ │ ├── AnimationCompletedCommand.java
│ │ │ │ │ ├── AnimationLockCommand.java
│ │ │ │ │ ├── AnimationProxy.java
│ │ │ │ │ ├── AnimationStartedCommand.java
│ │ │ │ │ ├── CardPlayedToken.java
│ │ │ │ │ ├── CardRevealedToken.java
│ │ │ │ │ ├── DamageEventVisualizer.java
│ │ │ │ │ ├── DamageNumber.java
│ │ │ │ │ ├── EventVisualizerDispatcher.java
│ │ │ │ │ ├── HealEventVisualizer.java
│ │ │ │ │ ├── HealingNumber.java
│ │ │ │ │ ├── IAnimationListener.java
│ │ │ │ │ ├── IGameEventVisualizer.java
│ │ │ │ │ ├── JoustToken.java
│ │ │ │ │ ├── JoustVisualizer.java
│ │ │ │ │ ├── PlayCardVisualizer.java
│ │ │ │ │ └── RevealCardVisualizer.java
│ │ │ │ └── config/
│ │ │ │ ├── PlayModeConfigMediator.java
│ │ │ │ ├── PlayModeConfigView.java
│ │ │ │ ├── PlayerConfigType.java
│ │ │ │ ├── RequestDeckFormatsCommand.java
│ │ │ │ └── RequestDecksCommand.java
│ │ │ ├── sandboxmode/
│ │ │ │ ├── CardCollectionEditor.java
│ │ │ │ ├── CardPanel.java
│ │ │ │ ├── EntityEditor.java
│ │ │ │ ├── GameTagEntry.java
│ │ │ │ ├── ICardCollectionEditingListener.java
│ │ │ │ ├── MinionPanel.java
│ │ │ │ ├── PlayerPanel.java
│ │ │ │ ├── SandboxEditor.java
│ │ │ │ ├── SandboxModeConfigView.java
│ │ │ │ ├── SandboxModeMediator.java
│ │ │ │ ├── SandboxModeView.java
│ │ │ │ ├── SandboxProxy.java
│ │ │ │ ├── ToolboxView.java
│ │ │ │ ├── actions/
│ │ │ │ │ ├── EditEntityAction.java
│ │ │ │ │ ├── KillAction.java
│ │ │ │ │ ├── SetManaAction.java
│ │ │ │ │ ├── SetMaxManaAction.java
│ │ │ │ │ └── SilenceAction.java
│ │ │ │ └── commands/
│ │ │ │ ├── CreateNewSandboxCommand.java
│ │ │ │ ├── ModifyPlayerDeckCommand.java
│ │ │ │ ├── ModifyPlayerHandCommand.java
│ │ │ │ ├── PerformActionCommand.java
│ │ │ │ ├── SelectPlayerCommand.java
│ │ │ │ ├── SpawnMinionCommand.java
│ │ │ │ ├── StartPlaySandboxCommand.java
│ │ │ │ └── StopPlaySandboxCommand.java
│ │ │ ├── simulationmode/
│ │ │ │ ├── PlayerConfigView.java
│ │ │ │ ├── PlayerInfoView.java
│ │ │ │ ├── SimulateGamesCommand.java
│ │ │ │ ├── SimulationMediator.java
│ │ │ │ ├── SimulationModeConfigView.java
│ │ │ │ ├── SimulationResult.java
│ │ │ │ ├── SimulationResultView.java
│ │ │ │ ├── StatEntry.java
│ │ │ │ └── WaitForSimulationView.java
│ │ │ └── trainingmode/
│ │ │ ├── PerformTrainingCommand.java
│ │ │ ├── RequestTrainingDataCommand.java
│ │ │ ├── SaveTrainingDataCommand.java
│ │ │ ├── TrainingConfig.java
│ │ │ ├── TrainingConfigView.java
│ │ │ ├── TrainingModeMediator.java
│ │ │ ├── TrainingModeView.java
│ │ │ ├── TrainingProgressReport.java
│ │ │ └── TrainingProxy.java
│ │ └── tools/
│ │ ├── CardCreator.java
│ │ ├── CardEditor.java
│ │ ├── EditorMainWindow.java
│ │ ├── ICardEditor.java
│ │ ├── ITextFieldAction.java
│ │ ├── IntegerListener.java
│ │ ├── MinionCardPanel.java
│ │ ├── SpellCardPanel.java
│ │ ├── SpellDescSerializer.java
│ │ ├── SpellStringConverter.java
│ │ └── WeaponClassPanel.java
│ └── resources/
│ ├── css/
│ │ ├── deckbuilder.css
│ │ ├── gameboard.css
│ │ ├── main.css
│ │ └── mainmenu.css
│ ├── fxml/
│ │ ├── BattleBatchResultToken.fxml
│ │ ├── BattleOfDecksConfigView.fxml
│ │ ├── BattleOfDecksResultView.fxml
│ │ ├── CardCollectionEditor.fxml
│ │ ├── CardEntry.fxml
│ │ ├── CardFilterView.fxml
│ │ ├── CardPanel.fxml
│ │ ├── CardTooltip.fxml
│ │ ├── CardView.fxml
│ │ ├── ChooseClassView.fxml
│ │ ├── DeckBuilderView.fxml
│ │ ├── DeckEntry.fxml
│ │ ├── DeckInfoView.fxml
│ │ ├── DeckListView.fxml
│ │ ├── DeckNameView.fxml
│ │ ├── DigitTemplate.fxml
│ │ ├── EditorMainWindow.fxml
│ │ ├── EntityEditor.fxml
│ │ ├── GameBoardView.fxml
│ │ ├── HandCard.fxml
│ │ ├── HeroToken.fxml
│ │ ├── HumanMulliganView.fxml
│ │ ├── LoadingBoardView.fxml
│ │ ├── MainMenuView.fxml
│ │ ├── MetaDeckListView.fxml
│ │ ├── MetaDeckView (2).fxml
│ │ ├── MetaDeckView.fxml
│ │ ├── MinionCardPanel.fxml
│ │ ├── MinionPanel.fxml
│ │ ├── PlayModeConfigView.fxml
│ │ ├── PlayModeView.fxml
│ │ ├── PlayerConfigView.fxml
│ │ ├── PlayerInfoView.fxml
│ │ ├── PlayerPanel.fxml
│ │ ├── SandboxModeConfigView.fxml
│ │ ├── SandboxModeView.fxml
│ │ ├── SimulationModeConfigView.fxml
│ │ ├── SimulationResultView.fxml
│ │ ├── SpellCardPanel.fxml
│ │ ├── SummonToken.fxml
│ │ ├── ToolboxView.fxml
│ │ ├── TrainingConfigView.fxml
│ │ ├── TrainingModeView.fxml
│ │ ├── UserDialog.fxml
│ │ └── WaitForSimulationView.fxml
│ └── logback.xml
├── build.gradle
├── cards/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── resources/
│ │ ├── cards/
│ │ │ ├── basic/
│ │ │ │ ├── druid/
│ │ │ │ │ ├── hero_malfurion.json
│ │ │ │ │ ├── hero_power_shapeshift.json
│ │ │ │ │ ├── minion_ironbark_protector.json
│ │ │ │ │ ├── spell_claw.json
│ │ │ │ │ ├── spell_excess_mana.json
│ │ │ │ │ ├── spell_healing_touch.json
│ │ │ │ │ ├── spell_innervate.json
│ │ │ │ │ ├── spell_mark_of_the_wild.json
│ │ │ │ │ ├── spell_moonfire.json
│ │ │ │ │ ├── spell_savage_roar.json
│ │ │ │ │ ├── spell_starfire.json
│ │ │ │ │ ├── spell_swipe.json
│ │ │ │ │ └── spell_wild_growth.json
│ │ │ │ ├── hunter/
│ │ │ │ │ ├── hero_power_steady_shot.json
│ │ │ │ │ ├── hero_rexxar.json
│ │ │ │ │ ├── minion_houndmaster.json
│ │ │ │ │ ├── minion_starving_buzzard.json
│ │ │ │ │ ├── minion_timber_wolf.json
│ │ │ │ │ ├── minion_tundra_rhino.json
│ │ │ │ │ ├── spell_animal_companion.json
│ │ │ │ │ ├── spell_arcane_shot.json
│ │ │ │ │ ├── spell_hunters_mark.json
│ │ │ │ │ ├── spell_kill_command.json
│ │ │ │ │ ├── spell_multi-shot.json
│ │ │ │ │ ├── spell_tracking.json
│ │ │ │ │ ├── token_huffer.json
│ │ │ │ │ ├── token_leokk.json
│ │ │ │ │ └── token_misha.json
│ │ │ │ ├── mage/
│ │ │ │ │ ├── hero_jaina.json
│ │ │ │ │ ├── hero_power_fireblast.json
│ │ │ │ │ ├── minion_water_elemental.json
│ │ │ │ │ ├── spell_arcane_explosion.json
│ │ │ │ │ ├── spell_arcane_intellect.json
│ │ │ │ │ ├── spell_arcane_missiles.json
│ │ │ │ │ ├── spell_fireball.json
│ │ │ │ │ ├── spell_flamestrike.json
│ │ │ │ │ ├── spell_frost_nova.json
│ │ │ │ │ ├── spell_frostbolt.json
│ │ │ │ │ ├── spell_mirror_image.json
│ │ │ │ │ ├── spell_polymorph.json
│ │ │ │ │ ├── token_mirror_image.json
│ │ │ │ │ └── token_sheep.json
│ │ │ │ ├── neutral/
│ │ │ │ │ ├── minion_acidic_swamp_ooze.json
│ │ │ │ │ ├── minion_archmage.json
│ │ │ │ │ ├── minion_bloodfen_raptor.json
│ │ │ │ │ ├── minion_bluegill_warrior.json
│ │ │ │ │ ├── minion_booty_bay_bodyguard.json
│ │ │ │ │ ├── minion_boulderfist_ogre.json
│ │ │ │ │ ├── minion_chillwind_yeti.json
│ │ │ │ │ ├── minion_core_hound.json
│ │ │ │ │ ├── minion_dalaran_mage.json
│ │ │ │ │ ├── minion_darkscale_healer.json
│ │ │ │ │ ├── minion_dragonling_mechanic.json
│ │ │ │ │ ├── minion_elven_archer.json
│ │ │ │ │ ├── minion_frostwolf_grunt.json
│ │ │ │ │ ├── minion_frostwolf_warlord.json
│ │ │ │ │ ├── minion_gnomish_inventor.json
│ │ │ │ │ ├── minion_goldshire_footman.json
│ │ │ │ │ ├── minion_grimscale_oracle.json
│ │ │ │ │ ├── minion_gurubashi_berserker.json
│ │ │ │ │ ├── minion_ironforge_rifleman.json
│ │ │ │ │ ├── minion_ironfur_grizzly.json
│ │ │ │ │ ├── minion_kobold_geomancer.json
│ │ │ │ │ ├── minion_lord_of_the_arena.json
│ │ │ │ │ ├── minion_magma_rager.json
│ │ │ │ │ ├── minion_murloc_raider.json
│ │ │ │ │ ├── minion_murloc_tidehunter.json
│ │ │ │ │ ├── minion_nightblade.json
│ │ │ │ │ ├── minion_novice_engineer.json
│ │ │ │ │ ├── minion_oasis_snapjaw.json
│ │ │ │ │ ├── minion_ogre_magi.json
│ │ │ │ │ ├── minion_raid_leader.json
│ │ │ │ │ ├── minion_razorfen_hunter.json
│ │ │ │ │ ├── minion_reckless_rocketeer.json
│ │ │ │ │ ├── minion_river_crocolisk.json
│ │ │ │ │ ├── minion_senjin_shieldmasta.json
│ │ │ │ │ ├── minion_shattered_sun_cleric.json
│ │ │ │ │ ├── minion_silverback_patriarch.json
│ │ │ │ │ ├── minion_stonetusk_boar.json
│ │ │ │ │ ├── minion_stormpike_commando.json
│ │ │ │ │ ├── minion_stormwind_champion.json
│ │ │ │ │ ├── minion_stormwind_knight.json
│ │ │ │ │ ├── minion_voodoo_doctor.json
│ │ │ │ │ ├── minion_war_golem.json
│ │ │ │ │ ├── minion_wolfrider.json
│ │ │ │ │ ├── spell_the_coin.json
│ │ │ │ │ ├── token_boar.json
│ │ │ │ │ ├── token_mechanical_dragonling.json
│ │ │ │ │ └── token_murloc_scout.json
│ │ │ │ ├── paladin/
│ │ │ │ │ ├── hero_power_reinforce.json
│ │ │ │ │ ├── hero_uther.json
│ │ │ │ │ ├── minion_guardian_of_kings.json
│ │ │ │ │ ├── spell_blessing_of_kings.json
│ │ │ │ │ ├── spell_blessing_of_might.json
│ │ │ │ │ ├── spell_consecration.json
│ │ │ │ │ ├── spell_hammer_of_wrath.json
│ │ │ │ │ ├── spell_hand_of_protection.json
│ │ │ │ │ ├── spell_holy_light.json
│ │ │ │ │ ├── spell_humility.json
│ │ │ │ │ ├── token_silver_hand_recruit.json
│ │ │ │ │ ├── weapon_lights_justice.json
│ │ │ │ │ └── weapon_truesilver_champion.json
│ │ │ │ ├── priest/
│ │ │ │ │ ├── hero_anduin.json
│ │ │ │ │ ├── hero_power_lesser_heal.json
│ │ │ │ │ ├── minion_northshire_cleric.json
│ │ │ │ │ ├── spell_divine_spirit.json
│ │ │ │ │ ├── spell_holy_nova.json
│ │ │ │ │ ├── spell_holy_smite.json
│ │ │ │ │ ├── spell_mind_blast.json
│ │ │ │ │ ├── spell_mind_control.json
│ │ │ │ │ ├── spell_mind_vision.json
│ │ │ │ │ ├── spell_power_word_shield.json
│ │ │ │ │ ├── spell_shadow_word_death.json
│ │ │ │ │ └── spell_shadow_word_pain.json
│ │ │ │ ├── rogue/
│ │ │ │ │ ├── hero_power_dagger_mastery.json
│ │ │ │ │ ├── hero_valeera.json
│ │ │ │ │ ├── spell_assassinate.json
│ │ │ │ │ ├── spell_backstab.json
│ │ │ │ │ ├── spell_deadly_poison.json
│ │ │ │ │ ├── spell_fan_of_knives.json
│ │ │ │ │ ├── spell_sap.json
│ │ │ │ │ ├── spell_shiv.json
│ │ │ │ │ ├── spell_sinister_strike.json
│ │ │ │ │ ├── spell_sprint.json
│ │ │ │ │ ├── spell_vanish.json
│ │ │ │ │ ├── weapon_assassins_blade.json
│ │ │ │ │ └── weapon_wicked_knife.json
│ │ │ │ ├── shaman/
│ │ │ │ │ ├── hero_power_totemic_call.json
│ │ │ │ │ ├── hero_thrall.json
│ │ │ │ │ ├── minion_fire_elemental.json
│ │ │ │ │ ├── minion_flametongue_totem.json
│ │ │ │ │ ├── minion_windspeaker.json
│ │ │ │ │ ├── spell_ancestral_healing.json
│ │ │ │ │ ├── spell_bloodlust.json
│ │ │ │ │ ├── spell_frost_shock.json
│ │ │ │ │ ├── spell_hex.json
│ │ │ │ │ ├── spell_rockbiter_weapon.json
│ │ │ │ │ ├── spell_totemic_might.json
│ │ │ │ │ ├── spell_windfury.json
│ │ │ │ │ ├── token_frog.json
│ │ │ │ │ ├── token_healing_totem.json
│ │ │ │ │ ├── token_searing_totem.json
│ │ │ │ │ ├── token_stoneclaw_totem.json
│ │ │ │ │ └── token_wrath_of_air_totem.json
│ │ │ │ ├── warlock/
│ │ │ │ │ ├── hero_guldan.json
│ │ │ │ │ ├── hero_power_life_tap.json
│ │ │ │ │ ├── minion_dread_infernal.json
│ │ │ │ │ ├── minion_succubus.json
│ │ │ │ │ ├── minion_voidwalker.json
│ │ │ │ │ ├── spell_corruption.json
│ │ │ │ │ ├── spell_drain_life.json
│ │ │ │ │ ├── spell_hellfire.json
│ │ │ │ │ ├── spell_mortal_coil.json
│ │ │ │ │ ├── spell_sacrificial_pact.json
│ │ │ │ │ ├── spell_shadow_bolt.json
│ │ │ │ │ └── spell_soulfire.json
│ │ │ │ └── warrior/
│ │ │ │ ├── hero_garrosh.json
│ │ │ │ ├── hero_power_armor_up.json
│ │ │ │ ├── minion_korkron_elite.json
│ │ │ │ ├── minion_warsong_commander.json
│ │ │ │ ├── spell_charge.json
│ │ │ │ ├── spell_cleave.json
│ │ │ │ ├── spell_execute.json
│ │ │ │ ├── spell_heroic_strike.json
│ │ │ │ ├── spell_shield_block.json
│ │ │ │ ├── spell_whirlwind.json
│ │ │ │ ├── weapon_arcanite_reaper.json
│ │ │ │ └── weapon_fiery_war_axe.json
│ │ │ ├── blackrock_mountain/
│ │ │ │ ├── hero_power_die_insect.json
│ │ │ │ ├── hero_ragnaros.json
│ │ │ │ ├── minion_axe_flinger.json
│ │ │ │ ├── minion_blackwing_corruptor.json
│ │ │ │ ├── minion_blackwing_technician.json
│ │ │ │ ├── minion_chromaggus.json
│ │ │ │ ├── minion_core_rager.json
│ │ │ │ ├── minion_dark_iron_skulker.json
│ │ │ │ ├── minion_dragon_consort.json
│ │ │ │ ├── minion_dragon_egg.json
│ │ │ │ ├── minion_dragonkin_sorcerer.json
│ │ │ │ ├── minion_drakonid_crusher.json
│ │ │ │ ├── minion_druid_of_the_flame.json
│ │ │ │ ├── minion_emperor_thaurissan.json
│ │ │ │ ├── minion_fireguard_destroyer.json
│ │ │ │ ├── minion_flamewaker.json
│ │ │ │ ├── minion_grim_patron.json
│ │ │ │ ├── minion_hungry_dragon.json
│ │ │ │ ├── minion_imp_gang_boss.json
│ │ │ │ ├── minion_majordomo_executus.json
│ │ │ │ ├── minion_nefarian.json
│ │ │ │ ├── minion_rend_blackhand.json
│ │ │ │ ├── minion_twilight_whelp.json
│ │ │ │ ├── minion_volcanic_drake.json
│ │ │ │ ├── minion_volcanic_lumberer.json
│ │ │ │ ├── spell_demonwrath.json
│ │ │ │ ├── spell_dragons_breath.json
│ │ │ │ ├── spell_gang_up.json
│ │ │ │ ├── spell_lava_shock.json
│ │ │ │ ├── spell_quick_shot.json
│ │ │ │ ├── spell_resurrect.json
│ │ │ │ ├── spell_revenge.json
│ │ │ │ ├── spell_solemn_vigil.json
│ │ │ │ ├── spell_tail_swipe.json
│ │ │ │ ├── token_black_whelp.json
│ │ │ │ ├── token_flame_bird_form.json
│ │ │ │ ├── token_flame_lion_form.json
│ │ │ │ └── token_flame_lionbird_form.json
│ │ │ ├── classic/
│ │ │ │ ├── druid/
│ │ │ │ │ ├── minion_ancient_of_lore.json
│ │ │ │ │ ├── minion_ancient_of_war.json
│ │ │ │ │ ├── minion_cenarius.json
│ │ │ │ │ ├── minion_druid_of_the_claw.json
│ │ │ │ │ ├── minion_keeper_of_the_grove.json
│ │ │ │ │ ├── spell_bite.json
│ │ │ │ │ ├── spell_force_of_nature.json
│ │ │ │ │ ├── spell_mark_of_nature.json
│ │ │ │ │ ├── spell_mark_of_nature_1.json
│ │ │ │ │ ├── spell_mark_of_nature_2.json
│ │ │ │ │ ├── spell_mark_of_nature_3.json
│ │ │ │ │ ├── spell_naturalize.json
│ │ │ │ │ ├── spell_nourish.json
│ │ │ │ │ ├── spell_nourish_1.json
│ │ │ │ │ ├── spell_nourish_2.json
│ │ │ │ │ ├── spell_nourish_3.json
│ │ │ │ │ ├── spell_power_of_the_wild.json
│ │ │ │ │ ├── spell_power_of_the_wild_1.json
│ │ │ │ │ ├── spell_power_of_the_wild_2.json
│ │ │ │ │ ├── spell_power_of_the_wild_3.json
│ │ │ │ │ ├── spell_savagery.json
│ │ │ │ │ ├── spell_soul_of_the_forest.json
│ │ │ │ │ ├── spell_starfall.json
│ │ │ │ │ ├── spell_starfall_1.json
│ │ │ │ │ ├── spell_starfall_2.json
│ │ │ │ │ ├── spell_starfall_3.json
│ │ │ │ │ ├── spell_wrath.json
│ │ │ │ │ ├── spell_wrath_1.json
│ │ │ │ │ ├── spell_wrath_2.json
│ │ │ │ │ ├── spell_wrath_3.json
│ │ │ │ │ ├── token_bear_form.json
│ │ │ │ │ ├── token_cat_form.json
│ │ │ │ │ ├── token_catbear_form.json
│ │ │ │ │ ├── token_panther.json
│ │ │ │ │ ├── token_treant.json
│ │ │ │ │ └── token_treant_taunt.json
│ │ │ │ ├── hunter/
│ │ │ │ │ ├── minion_king_krush.json
│ │ │ │ │ ├── minion_savannah_highmane.json
│ │ │ │ │ ├── minion_scavenging_hyena.json
│ │ │ │ │ ├── secret_explosive_trap.json
│ │ │ │ │ ├── secret_freezing_trap.json
│ │ │ │ │ ├── secret_misdirection.json
│ │ │ │ │ ├── secret_snake_trap.json
│ │ │ │ │ ├── secret_snipe.json
│ │ │ │ │ ├── spell_bestial_wrath.json
│ │ │ │ │ ├── spell_deadly_shot.json
│ │ │ │ │ ├── spell_explosive_shot.json
│ │ │ │ │ ├── spell_flare.json
│ │ │ │ │ ├── spell_unleash_the_hounds.json
│ │ │ │ │ ├── token_hound.json
│ │ │ │ │ ├── token_hyena.json
│ │ │ │ │ ├── token_snake.json
│ │ │ │ │ ├── weapon_eaglehorn_bow.json
│ │ │ │ │ └── weapon_gladiators_longbow.json
│ │ │ │ ├── mage/
│ │ │ │ │ ├── minion_archmage_antonidas.json
│ │ │ │ │ ├── minion_ethereal_arcanist.json
│ │ │ │ │ ├── minion_kirin_tor_mage.json
│ │ │ │ │ ├── minion_mana_wyrm.json
│ │ │ │ │ ├── minion_sorcerers_apprentice.json
│ │ │ │ │ ├── secret_counterspell.json
│ │ │ │ │ ├── secret_ice_barrier.json
│ │ │ │ │ ├── secret_ice_block.json
│ │ │ │ │ ├── secret_mirror_entity.json
│ │ │ │ │ ├── secret_spellbender.json
│ │ │ │ │ ├── secret_vaporize.json
│ │ │ │ │ ├── spell_blizzard.json
│ │ │ │ │ ├── spell_cone_of_cold.json
│ │ │ │ │ ├── spell_pyroblast.json
│ │ │ │ │ └── token_spellbender.json
│ │ │ │ ├── neutral/
│ │ │ │ │ ├── minion_abomination.json
│ │ │ │ │ ├── minion_abusive_sergeant.json
│ │ │ │ │ ├── minion_acolyte_of_pain.json
│ │ │ │ │ ├── minion_alarm-o-bot.json
│ │ │ │ │ ├── minion_alexstrasza.json
│ │ │ │ │ ├── minion_amani_berserker.json
│ │ │ │ │ ├── minion_ancient_brewmaster.json
│ │ │ │ │ ├── minion_ancient_mage.json
│ │ │ │ │ ├── minion_ancient_watcher.json
│ │ │ │ │ ├── minion_angry_chicken.json
│ │ │ │ │ ├── minion_arcane_golem.json
│ │ │ │ │ ├── minion_argent_commander.json
│ │ │ │ │ ├── minion_argent_squire.json
│ │ │ │ │ ├── minion_baron_geddon.json
│ │ │ │ │ ├── minion_big_game_hunter.json
│ │ │ │ │ ├── minion_blood_knight.json
│ │ │ │ │ ├── minion_bloodmage_thalnos.json
│ │ │ │ │ ├── minion_bloodsail_corsair.json
│ │ │ │ │ ├── minion_bloodsail_raider.json
│ │ │ │ │ ├── minion_cairne_bloodhoof.json
│ │ │ │ │ ├── minion_captain_greenskin.json
│ │ │ │ │ ├── minion_coldlight_oracle.json
│ │ │ │ │ ├── minion_coldlight_seer.json
│ │ │ │ │ ├── minion_crazed_alchemist.json
│ │ │ │ │ ├── minion_cult_master.json
│ │ │ │ │ ├── minion_dark_iron_dwarf.json
│ │ │ │ │ ├── minion_deathwing.json
│ │ │ │ │ ├── minion_defender_of_argus.json
│ │ │ │ │ ├── minion_demolisher.json
│ │ │ │ │ ├── minion_dire_wolf_alpha.json
│ │ │ │ │ ├── minion_doomsayer.json
│ │ │ │ │ ├── minion_dread_corsair.json
│ │ │ │ │ ├── minion_earthen_ring_farseer.json
│ │ │ │ │ ├── minion_emperor_cobra.json
│ │ │ │ │ ├── minion_faceless_manipulator.json
│ │ │ │ │ ├── minion_faerie_dragon.json
│ │ │ │ │ ├── minion_fen_creeper.json
│ │ │ │ │ ├── minion_flesheating_ghoul.json
│ │ │ │ │ ├── minion_frost_elemental.json
│ │ │ │ │ ├── minion_gadgetzan_auctioneer.json
│ │ │ │ │ ├── minion_gruul.json
│ │ │ │ │ ├── minion_harrison_jones.json
│ │ │ │ │ ├── minion_harvest_golem.json
│ │ │ │ │ ├── minion_hogger.json
│ │ │ │ │ ├── minion_hungry_crab.json
│ │ │ │ │ ├── minion_illidan_stormrage.json
│ │ │ │ │ ├── minion_imp_master.json
│ │ │ │ │ ├── minion_injured_blademaster.json
│ │ │ │ │ ├── minion_ironbeak_owl.json
│ │ │ │ │ ├── minion_jungle_panther.json
│ │ │ │ │ ├── minion_king_mukla.json
│ │ │ │ │ ├── minion_knife_juggler.json
│ │ │ │ │ ├── minion_leeroy_jenkins.json
│ │ │ │ │ ├── minion_leper_gnome.json
│ │ │ │ │ ├── minion_lightwarden.json
│ │ │ │ │ ├── minion_loot_hoarder.json
│ │ │ │ │ ├── minion_lorewalker_cho.json
│ │ │ │ │ ├── minion_mad_bomber.json
│ │ │ │ │ ├── minion_malygos.json
│ │ │ │ │ ├── minion_mana_addict.json
│ │ │ │ │ ├── minion_mana_wraith.json
│ │ │ │ │ ├── minion_master_swordsmith.json
│ │ │ │ │ ├── minion_millhouse_manastorm.json
│ │ │ │ │ ├── minion_mind_control_tech.json
│ │ │ │ │ ├── minion_mogushan_warden.json
│ │ │ │ │ ├── minion_molten_giant.json
│ │ │ │ │ ├── minion_mountain_giant.json
│ │ │ │ │ ├── minion_murloc_tidecaller.json
│ │ │ │ │ ├── minion_murloc_warleader.json
│ │ │ │ │ ├── minion_nat_pagle.json
│ │ │ │ │ ├── minion_nozdormu.json
│ │ │ │ │ ├── minion_onyxia.json
│ │ │ │ │ ├── minion_pint-sized_summoner.json
│ │ │ │ │ ├── minion_priestess_of_elune.json
│ │ │ │ │ ├── minion_questing_adventurer.json
│ │ │ │ │ ├── minion_raging_worgen.json
│ │ │ │ │ ├── minion_ravenholdt_assassin.json
│ │ │ │ │ ├── minion_scarlet_crusader.json
│ │ │ │ │ ├── minion_sea_giant.json
│ │ │ │ │ ├── minion_secretkeeper.json
│ │ │ │ │ ├── minion_shieldbearer.json
│ │ │ │ │ ├── minion_silver_hand_knight.json
│ │ │ │ │ ├── minion_silvermoon_guardian.json
│ │ │ │ │ ├── minion_southsea_captain.json
│ │ │ │ │ ├── minion_southsea_deckhand.json
│ │ │ │ │ ├── minion_spellbreaker.json
│ │ │ │ │ ├── minion_spiteful_smith.json
│ │ │ │ │ ├── minion_stampeding_kodo.json
│ │ │ │ │ ├── minion_stranglethorn_tiger.json
│ │ │ │ │ ├── minion_sunfury_protector.json
│ │ │ │ │ ├── minion_sunwalker.json
│ │ │ │ │ ├── minion_tauren_warrior.json
│ │ │ │ │ ├── minion_the_beast.json
│ │ │ │ │ ├── minion_the_black_knight.json
│ │ │ │ │ ├── minion_thrallmar_farseer.json
│ │ │ │ │ ├── minion_tinkmaster_overspark.json
│ │ │ │ │ ├── minion_twilight_drake.json
│ │ │ │ │ ├── minion_venture_co_mercenary.json
│ │ │ │ │ ├── minion_violet_teacher.json
│ │ │ │ │ ├── minion_wild_pyromancer.json
│ │ │ │ │ ├── minion_windfury_harpy.json
│ │ │ │ │ ├── minion_wisp.json
│ │ │ │ │ ├── minion_worgen_infiltrator.json
│ │ │ │ │ ├── minion_young_dragonhawk.json
│ │ │ │ │ ├── minion_young_priestess.json
│ │ │ │ │ ├── minion_youthful_brewmaster.json
│ │ │ │ │ ├── minion_ysera.json
│ │ │ │ │ ├── spell_bananas.json
│ │ │ │ │ ├── spell_dream.json
│ │ │ │ │ ├── spell_nightmare.json
│ │ │ │ │ ├── spell_ysera_awakens.json
│ │ │ │ │ ├── token_baine_bloodhoof.json
│ │ │ │ │ ├── token_chicken.json
│ │ │ │ │ ├── token_damaged_golem.json
│ │ │ │ │ ├── token_devilsaur.json
│ │ │ │ │ ├── token_emerald_drake.json
│ │ │ │ │ ├── token_finkle_einhorn.json
│ │ │ │ │ ├── token_flame_of_azzinoth.json
│ │ │ │ │ ├── token_gnoll.json
│ │ │ │ │ ├── token_imp.json
│ │ │ │ │ ├── token_laughing_sister.json
│ │ │ │ │ ├── token_murloc.json
│ │ │ │ │ ├── token_squire.json
│ │ │ │ │ ├── token_squirrel.json
│ │ │ │ │ ├── token_violet_apprentice.json
│ │ │ │ │ └── token_whelp.json
│ │ │ │ ├── paladin/
│ │ │ │ │ ├── minion_aldor_peacekeeper.json
│ │ │ │ │ ├── minion_argent_protector.json
│ │ │ │ │ ├── minion_tirion_fordring.json
│ │ │ │ │ ├── secret_eye_for_an_eye.json
│ │ │ │ │ ├── secret_noble_sacrifice.json
│ │ │ │ │ ├── secret_redemption.json
│ │ │ │ │ ├── secret_repentance.json
│ │ │ │ │ ├── spell_avenging_wrath.json
│ │ │ │ │ ├── spell_blessed_champion.json
│ │ │ │ │ ├── spell_blessing_of_wisdom.json
│ │ │ │ │ ├── spell_divine_favor.json
│ │ │ │ │ ├── spell_equality.json
│ │ │ │ │ ├── spell_holy_wrath.json
│ │ │ │ │ ├── spell_lay_on_hands.json
│ │ │ │ │ ├── token_defender.json
│ │ │ │ │ ├── weapon_ashbringer.json
│ │ │ │ │ └── weapon_sword_of_justice.json
│ │ │ │ ├── priest/
│ │ │ │ │ ├── hero_power_mind_shatter.json
│ │ │ │ │ ├── hero_power_mind_spike.json
│ │ │ │ │ ├── minion_auchenai_soulpriest.json
│ │ │ │ │ ├── minion_cabal_shadow_priest.json
│ │ │ │ │ ├── minion_lightspawn.json
│ │ │ │ │ ├── minion_lightwell.json
│ │ │ │ │ ├── minion_prophet_velen.json
│ │ │ │ │ ├── minion_temple_enforcer.json
│ │ │ │ │ ├── spell_circle_of_healing.json
│ │ │ │ │ ├── spell_holy_fire.json
│ │ │ │ │ ├── spell_inner_fire.json
│ │ │ │ │ ├── spell_mass_dispel.json
│ │ │ │ │ ├── spell_mindgames.json
│ │ │ │ │ ├── spell_shadow_madness.json
│ │ │ │ │ ├── spell_shadowform.json
│ │ │ │ │ ├── spell_silence.json
│ │ │ │ │ ├── spell_thoughtsteal.json
│ │ │ │ │ └── token_shadow_of_nothing.json
│ │ │ │ ├── rogue/
│ │ │ │ │ ├── minion_defias_ringleader.json
│ │ │ │ │ ├── minion_edwin_vancleef.json
│ │ │ │ │ ├── minion_kidnapper.json
│ │ │ │ │ ├── minion_master_of_disguise.json
│ │ │ │ │ ├── minion_patient_assassin.json
│ │ │ │ │ ├── minion_si7_agent.json
│ │ │ │ │ ├── spell_betrayal.json
│ │ │ │ │ ├── spell_blade_flurry.json
│ │ │ │ │ ├── spell_cold_blood.json
│ │ │ │ │ ├── spell_eviscerate.json
│ │ │ │ │ ├── spell_headcrack.json
│ │ │ │ │ ├── spell_preparation.json
│ │ │ │ │ ├── spell_shadowstep.json
│ │ │ │ │ ├── token_defias_bandit.json
│ │ │ │ │ └── weapon_perditions_blade.json
│ │ │ │ ├── shaman/
│ │ │ │ │ ├── minion_al_akir_the_windlord.json
│ │ │ │ │ ├── minion_dust_devil.json
│ │ │ │ │ ├── minion_earth_elemental.json
│ │ │ │ │ ├── minion_mana_tide_totem.json
│ │ │ │ │ ├── minion_unbound_elemental.json
│ │ │ │ │ ├── spell_ancestral_spirit.json
│ │ │ │ │ ├── spell_earth_shock.json
│ │ │ │ │ ├── spell_far_sight.json
│ │ │ │ │ ├── spell_feral_spirit.json
│ │ │ │ │ ├── spell_forked_lightning.json
│ │ │ │ │ ├── spell_lava_burst.json
│ │ │ │ │ ├── spell_lightning_bolt.json
│ │ │ │ │ ├── spell_lightning_storm.json
│ │ │ │ │ ├── token_spirit_wolf.json
│ │ │ │ │ ├── weapon_doomhammer.json
│ │ │ │ │ └── weapon_stormforged_axe.json
│ │ │ │ ├── warlock/
│ │ │ │ │ ├── hero_jaraxxus.json
│ │ │ │ │ ├── hero_power_inferno.json
│ │ │ │ │ ├── minion_blood_imp.json
│ │ │ │ │ ├── minion_doomguard.json
│ │ │ │ │ ├── minion_felguard.json
│ │ │ │ │ ├── minion_flame_imp.json
│ │ │ │ │ ├── minion_lord_jaraxxus.json
│ │ │ │ │ ├── minion_pit_lord.json
│ │ │ │ │ ├── minion_summoning_portal.json
│ │ │ │ │ ├── minion_void_terror.json
│ │ │ │ │ ├── spell_bane_of_doom.json
│ │ │ │ │ ├── spell_demonfire.json
│ │ │ │ │ ├── spell_sense_demons.json
│ │ │ │ │ ├── spell_shadowflame.json
│ │ │ │ │ ├── spell_siphon_soul.json
│ │ │ │ │ ├── spell_twisting_nether.json
│ │ │ │ │ ├── token_infernal.json
│ │ │ │ │ ├── token_worthless_imp.json
│ │ │ │ │ └── weapon_blood_fury.json
│ │ │ │ └── warrior/
│ │ │ │ ├── minion_arathi_weaponsmith.json
│ │ │ │ ├── minion_armorsmith.json
│ │ │ │ ├── minion_cruel_taskmaster.json
│ │ │ │ ├── minion_frothing_berserker.json
│ │ │ │ ├── minion_grommash_hellscream.json
│ │ │ │ ├── spell_battle_rage.json
│ │ │ │ ├── spell_brawl.json
│ │ │ │ ├── spell_commanding_shout.json
│ │ │ │ ├── spell_inner_rage.json
│ │ │ │ ├── spell_mortal_strike.json
│ │ │ │ ├── spell_rampage.json
│ │ │ │ ├── spell_shield_slam.json
│ │ │ │ ├── spell_slam.json
│ │ │ │ ├── spell_upgrade.json
│ │ │ │ ├── weapon_battle_axe.json
│ │ │ │ ├── weapon_gorehowl.json
│ │ │ │ └── weapon_heavy_axe.json
│ │ │ ├── goblins_vs_gnomes/
│ │ │ │ ├── druid/
│ │ │ │ │ ├── minion_anodized_robo_cub.json
│ │ │ │ │ ├── minion_druid_of_the_fang.json
│ │ │ │ │ ├── minion_grove_tender.json
│ │ │ │ │ ├── minion_malorne.json
│ │ │ │ │ ├── minion_mech-bear-cat.json
│ │ │ │ │ ├── spell_dark_wispers.json
│ │ │ │ │ ├── spell_dark_wispers_1.json
│ │ │ │ │ ├── spell_dark_wispers_2.json
│ │ │ │ │ ├── spell_dark_wispers_3.json
│ │ │ │ │ ├── spell_recycle.json
│ │ │ │ │ ├── spell_tree_of_life.json
│ │ │ │ │ └── token_cobra_form.json
│ │ │ │ ├── hunter/
│ │ │ │ │ ├── minion_gahzrilla.json
│ │ │ │ │ ├── minion_king_of_beasts.json
│ │ │ │ │ ├── minion_metaltooth_leaper.json
│ │ │ │ │ ├── minion_steamwheedle_sniper.json
│ │ │ │ │ ├── spell_call_pet.json
│ │ │ │ │ ├── spell_cobra_shot.json
│ │ │ │ │ ├── spell_feign_death.json
│ │ │ │ │ └── weapon_glaivezooka.json
│ │ │ │ ├── mage/
│ │ │ │ │ ├── minion_flame_leviathan.json
│ │ │ │ │ ├── minion_goblin_blastmage.json
│ │ │ │ │ ├── minion_snowchugger.json
│ │ │ │ │ ├── minion_soot_spewer.json
│ │ │ │ │ ├── minion_wee_spellstopper.json
│ │ │ │ │ ├── spell_echo_of_medivh.json
│ │ │ │ │ ├── spell_flamecannon.json
│ │ │ │ │ └── spell_unstable_portal.json
│ │ │ │ ├── neutral/
│ │ │ │ │ ├── minion_annoy-o-tron.json
│ │ │ │ │ ├── minion_antique_healbot.json
│ │ │ │ │ ├── minion_arcane_nullifier_x-21.json
│ │ │ │ │ ├── minion_blingtron_3000.json
│ │ │ │ │ ├── minion_bomb_lobber.json
│ │ │ │ │ ├── minion_burly_rockjaw_trogg.json
│ │ │ │ │ ├── minion_clockwork_giant.json
│ │ │ │ │ ├── minion_clockwork_gnome.json
│ │ │ │ │ ├── minion_cogmaster.json
│ │ │ │ │ ├── minion_dr_boom.json
│ │ │ │ │ ├── minion_enhance-o_mechano.json
│ │ │ │ │ ├── minion_explosive_sheep.json
│ │ │ │ │ ├── minion_fel_reaver.json
│ │ │ │ │ ├── minion_flying_machine.json
│ │ │ │ │ ├── minion_foe_reaper_4000.json
│ │ │ │ │ ├── minion_force-tank_max.json
│ │ │ │ │ ├── minion_gazlowe.json
│ │ │ │ │ ├── minion_gilblin_stalker.json
│ │ │ │ │ ├── minion_gnomeregan_infantry.json
│ │ │ │ │ ├── minion_gnomish_experimenter.json
│ │ │ │ │ ├── minion_goblin_sapper.json
│ │ │ │ │ ├── minion_hemet_nesingwary.json
│ │ │ │ │ ├── minion_hobgoblin.json
│ │ │ │ │ ├── minion_illuminator.json
│ │ │ │ │ ├── minion_jeeves.json
│ │ │ │ │ ├── minion_junkbot.json
│ │ │ │ │ ├── minion_kezan_mystic.json
│ │ │ │ │ ├── minion_lil_exorcist.json
│ │ │ │ │ ├── minion_lost_tallstrider.json
│ │ │ │ │ ├── minion_madder_bomber.json
│ │ │ │ │ ├── minion_mechanical_yeti.json
│ │ │ │ │ ├── minion_mechwarper.json
│ │ │ │ │ ├── minion_mekgineer_thermaplugg.json
│ │ │ │ │ ├── minion_micro_machine.json
│ │ │ │ │ ├── minion_mimirons_head.json
│ │ │ │ │ ├── minion_mini-mage.json
│ │ │ │ │ ├── minion_mogor_the_ogre.json
│ │ │ │ │ ├── minion_ogre_brute.json
│ │ │ │ │ ├── minion_piloted_shredder.json
│ │ │ │ │ ├── minion_piloted_sky_golem.json
│ │ │ │ │ ├── minion_puddlestomper.json
│ │ │ │ │ ├── minion_recombobulator.json
│ │ │ │ │ ├── minion_salty_dog.json
│ │ │ │ │ ├── minion_ships_cannon.json
│ │ │ │ │ ├── minion_sneeds_old_shredder.json
│ │ │ │ │ ├── minion_spider_tank.json
│ │ │ │ │ ├── minion_stonesplinter_trogg.json
│ │ │ │ │ ├── minion_target_dummy.json
│ │ │ │ │ ├── minion_tinkertown_technician.json
│ │ │ │ │ ├── minion_toshley.json
│ │ │ │ │ ├── minion_troggzor_the_earthinator.json
│ │ │ │ │ ├── token_boom_bot.json
│ │ │ │ │ ├── token_chicken_gvg.json
│ │ │ │ │ └── token_v-07-tr-0n.json
│ │ │ │ ├── paladin/
│ │ │ │ │ ├── minion_bolvar_fordragon.json
│ │ │ │ │ ├── minion_cobalt_guardian.json
│ │ │ │ │ ├── minion_quartermaster.json
│ │ │ │ │ ├── minion_scarlet_purifier.json
│ │ │ │ │ ├── minion_shielded_minibot.json
│ │ │ │ │ ├── spell_muster_for_battle.json
│ │ │ │ │ ├── spell_seal_of_light.json
│ │ │ │ │ └── weapon_coghammer.json
│ │ │ │ ├── priest/
│ │ │ │ │ ├── minion_shadowbomber.json
│ │ │ │ │ ├── minion_shadowboxer.json
│ │ │ │ │ ├── minion_shrinkmeister.json
│ │ │ │ │ ├── minion_upgraded_repair_bot.json
│ │ │ │ │ ├── minion_voljin.json
│ │ │ │ │ ├── spell_light_of_the_naaru.json
│ │ │ │ │ ├── spell_lightbomb.json
│ │ │ │ │ └── spell_velens_chosen.json
│ │ │ │ ├── rogue/
│ │ │ │ │ ├── minion_goblin_auto-barber.json
│ │ │ │ │ ├── minion_iron_sensei.json
│ │ │ │ │ ├── minion_ogre_ninja.json
│ │ │ │ │ ├── minion_one-eyed_cheat.json
│ │ │ │ │ ├── minion_trade_prince_gallywix.json
│ │ │ │ │ ├── spell_gallywixs_coin.json
│ │ │ │ │ ├── spell_sabotage.json
│ │ │ │ │ ├── spell_tinkers_sharpsword_oil.json
│ │ │ │ │ └── weapon_cogmasters_wrench.json
│ │ │ │ ├── shaman/
│ │ │ │ │ ├── minion_dunemaul_shaman.json
│ │ │ │ │ ├── minion_neptulon.json
│ │ │ │ │ ├── minion_siltfin_spiritwalker.json
│ │ │ │ │ ├── minion_vitality_totem.json
│ │ │ │ │ ├── minion_whirling_zap-o-matic.json
│ │ │ │ │ ├── spell_ancestors_call.json
│ │ │ │ │ ├── spell_crackle.json
│ │ │ │ │ └── weapon_powermace.json
│ │ │ │ ├── spare_parts/
│ │ │ │ │ ├── spell_armor_plating.json
│ │ │ │ │ ├── spell_emergency_coolant.json
│ │ │ │ │ ├── spell_finicky_cloakfield.json
│ │ │ │ │ ├── spell_reversing_switch.json
│ │ │ │ │ ├── spell_rusty_horn.json
│ │ │ │ │ ├── spell_time_rewinder.json
│ │ │ │ │ └── spell_whirling_blades.json
│ │ │ │ ├── warlock/
│ │ │ │ │ ├── minion_anima_golem.json
│ │ │ │ │ ├── minion_fel_cannon.json
│ │ │ │ │ ├── minion_floating_watcher.json
│ │ │ │ │ ├── minion_malganis.json
│ │ │ │ │ ├── minion_mistress_of_pain.json
│ │ │ │ │ ├── spell_darkbomb.json
│ │ │ │ │ ├── spell_demonheart.json
│ │ │ │ │ └── spell_imp-losion.json
│ │ │ │ └── warrior/
│ │ │ │ ├── minion_iron_juggernaut.json
│ │ │ │ ├── minion_screwjank_clunker.json
│ │ │ │ ├── minion_shieldmaiden.json
│ │ │ │ ├── minion_siege_engine.json
│ │ │ │ ├── minion_warbot.json
│ │ │ │ ├── spell_bouncing_blade.json
│ │ │ │ ├── spell_burrowing_mine.json
│ │ │ │ ├── spell_crush.json
│ │ │ │ └── weapon_ogre_warmaul.json
│ │ │ ├── hall_of_fame/
│ │ │ │ ├── minion_azure_drake.json
│ │ │ │ ├── minion_captains_parrot.json
│ │ │ │ ├── minion_elite_tauren_chieftain.json
│ │ │ │ ├── minion_gelbin_mekkatorque.json
│ │ │ │ ├── minion_old_murk-eye.json
│ │ │ │ ├── minion_ragnaros_the_firelord.json
│ │ │ │ ├── minion_sylvanas_windrunner.json
│ │ │ │ ├── spell_conceal.json
│ │ │ │ ├── spell_ice_lance.json
│ │ │ │ └── spell_power_overwhelming.json
│ │ │ ├── league_of_explorers/
│ │ │ │ ├── minion_ancient_shade.json
│ │ │ │ ├── minion_animated_armor.json
│ │ │ │ ├── minion_anubisath_sentinel.json
│ │ │ │ ├── minion_archthief_rafaam.json
│ │ │ │ ├── minion_brann_bronzebeard.json
│ │ │ │ ├── minion_dark_peddler.json
│ │ │ │ ├── minion_desert_camel.json
│ │ │ │ ├── minion_djinni_of_zephyrs.json
│ │ │ │ ├── minion_eerie_statue.json
│ │ │ │ ├── minion_elise_starseeker.json
│ │ │ │ ├── minion_ethereal_conjurer.json
│ │ │ │ ├── minion_fierce_monkey.json
│ │ │ │ ├── minion_fossilized_devilsaur.json
│ │ │ │ ├── minion_gorillabot_a3.json
│ │ │ │ ├── minion_huge_toad.json
│ │ │ │ ├── minion_jeweled_scarab.json
│ │ │ │ ├── minion_jungle_moonkin.json
│ │ │ │ ├── minion_keeper_of_uldaman.json
│ │ │ │ ├── minion_mounted_raptor.json
│ │ │ │ ├── minion_murloc_tinyfin.json
│ │ │ │ ├── minion_museum_curator.json
│ │ │ │ ├── minion_naga_sea_witch.json
│ │ │ │ ├── minion_obsidian_destroyer.json
│ │ │ │ ├── minion_pit_snake.json
│ │ │ │ ├── minion_reliquary_seeker.json
│ │ │ │ ├── minion_reno_jackson.json
│ │ │ │ ├── minion_rumbling_elemental.json
│ │ │ │ ├── minion_sir_finley_mrrgglton.json
│ │ │ │ ├── minion_summoning_stone.json
│ │ │ │ ├── minion_tomb_pillager.json
│ │ │ │ ├── minion_tomb_spider.json
│ │ │ │ ├── minion_tunnel_trogg.json
│ │ │ │ ├── minion_unearthed_raptor.json
│ │ │ │ ├── minion_wobbling_runts.json
│ │ │ │ ├── secret_dart_trap.json
│ │ │ │ ├── secret_sacred_trial.json
│ │ │ │ ├── spell_ancient_curse.json
│ │ │ │ ├── spell_anyfin_can_happen.json
│ │ │ │ ├── spell_curse_of_rafaam.json
│ │ │ │ ├── spell_cursed.json
│ │ │ │ ├── spell_entomb.json
│ │ │ │ ├── spell_everyfin_is_awesome.json
│ │ │ │ ├── spell_excavated_evil.json
│ │ │ │ ├── spell_explorers_hat.json
│ │ │ │ ├── spell_forgotten_torch.json
│ │ │ │ ├── spell_lantern_of_power.json
│ │ │ │ ├── spell_map_to_the_golden_monkey.json
│ │ │ │ ├── spell_mirror_of_doom.json
│ │ │ │ ├── spell_raven_idol.json
│ │ │ │ ├── spell_raven_idol_1.json
│ │ │ │ ├── spell_raven_idol_2.json
│ │ │ │ ├── spell_raven_idol_3.json
│ │ │ │ ├── spell_roaring_torch.json
│ │ │ │ ├── spell_timepiece_of_horror.json
│ │ │ │ ├── token_golden_monkey.json
│ │ │ │ ├── token_grumbly_runt.json
│ │ │ │ ├── token_mummy_zombie.json
│ │ │ │ ├── token_rascally_runt.json
│ │ │ │ ├── token_scarab.json
│ │ │ │ ├── token_wily_runt.json
│ │ │ │ └── weapon_cursed_blade.json
│ │ │ ├── mean_streets_of_gadgetzan/
│ │ │ │ ├── druid/
│ │ │ │ │ ├── minion_celestial_dreamer.json
│ │ │ │ │ ├── minion_jade_behemoth.json
│ │ │ │ │ ├── minion_kun_the_forgotten_king.json
│ │ │ │ │ ├── minion_virmen_sensei.json
│ │ │ │ │ ├── spell_jade_blossom.json
│ │ │ │ │ ├── spell_jade_idol.json
│ │ │ │ │ ├── spell_jade_idol_1.json
│ │ │ │ │ ├── spell_jade_idol_2.json
│ │ │ │ │ ├── spell_jade_idol_3.json
│ │ │ │ │ ├── spell_lunar_visions.json
│ │ │ │ │ ├── spell_mark_of_the_lotus.json
│ │ │ │ │ └── spell_pilfered_power.json
│ │ │ │ ├── grimy_goons/
│ │ │ │ │ ├── minion_don_hancho.json
│ │ │ │ │ ├── minion_grimestreet_informant.json
│ │ │ │ │ └── minion_grimestreet_smuggler.json
│ │ │ │ ├── hunter/
│ │ │ │ │ ├── minion_alleycat.json
│ │ │ │ │ ├── minion_dispatch_kodo.json
│ │ │ │ │ ├── minion_knuckles.json
│ │ │ │ │ ├── minion_rat_pack.json
│ │ │ │ │ ├── minion_shaky_zipgunner.json
│ │ │ │ │ ├── minion_trogg_beastrager.json
│ │ │ │ │ ├── secret_hidden_cache.json
│ │ │ │ │ ├── spell_smugglers_crate.json
│ │ │ │ │ ├── token_piranha.json
│ │ │ │ │ ├── token_rat.json
│ │ │ │ │ ├── token_tabbycat.json
│ │ │ │ │ └── weapon_piranha_launcher.json
│ │ │ │ ├── jade_lotus/
│ │ │ │ │ ├── minion_aya_blackpaw.json
│ │ │ │ │ ├── minion_jade_spirit.json
│ │ │ │ │ └── minion_lotus_agents.json
│ │ │ │ ├── kabal/
│ │ │ │ │ ├── minion_kabal_chemist.json
│ │ │ │ │ ├── minion_kabal_courier.json
│ │ │ │ │ ├── minion_kazakus.json
│ │ │ │ │ ├── token_greater_demon.json
│ │ │ │ │ ├── token_kabal_sheep.json
│ │ │ │ │ ├── token_lesser_demon.json
│ │ │ │ │ └── token_superior_demon.json
│ │ │ │ ├── mage/
│ │ │ │ │ ├── hero_baaraxxus.json
│ │ │ │ │ ├── minion_cryomancer.json
│ │ │ │ │ ├── minion_inkmaster_solia.json
│ │ │ │ │ ├── minion_kabal_crystal_runner.json
│ │ │ │ │ ├── minion_kabal_lackey.json
│ │ │ │ │ ├── minion_manic_soulcaster.json
│ │ │ │ │ ├── secret_potion_of_polymorph.json
│ │ │ │ │ ├── spell_freezing_potion.json
│ │ │ │ │ ├── spell_greater_arcane_missiles.json
│ │ │ │ │ └── spell_volcanic_potion.json
│ │ │ │ ├── neutral/
│ │ │ │ │ ├── minion_ancient_of_blossoms.json
│ │ │ │ │ ├── minion_auctionmaster_beardo.json
│ │ │ │ │ ├── minion_backroom_bouncer.json
│ │ │ │ │ ├── minion_backstreet_leper.json
│ │ │ │ │ ├── minion_big-time_racketeer.json
│ │ │ │ │ ├── minion_blowgill_sniper.json
│ │ │ │ │ ├── minion_blubber_baron.json
│ │ │ │ │ ├── minion_bomb_squad.json
│ │ │ │ │ ├── minion_burgly_bully.json
│ │ │ │ │ ├── minion_daring_reporter.json
│ │ │ │ │ ├── minion_defias_cleaner.json
│ │ │ │ │ ├── minion_dirty_rat.json
│ │ │ │ │ ├── minion_doppelgangster.json
│ │ │ │ │ ├── minion_fel_orc_soulfiend.json
│ │ │ │ │ ├── minion_fight_promoter.json
│ │ │ │ │ ├── minion_finja_the_flying_star.json
│ │ │ │ │ ├── minion_friendly_bartender.json
│ │ │ │ │ ├── minion_gadgetzan_socialite.json
│ │ │ │ │ ├── minion_genzo_the_shark.json
│ │ │ │ │ ├── minion_grook_fu_master.json
│ │ │ │ │ ├── minion_hired_gun.json
│ │ │ │ │ ├── minion_hozen_healer.json
│ │ │ │ │ ├── minion_kooky_chemist.json
│ │ │ │ │ ├── minion_leatherclad_hogleader.json
│ │ │ │ │ ├── minion_madam_goya.json
│ │ │ │ │ ├── minion_mayor_noggenfogger.json
│ │ │ │ │ ├── minion_mistress_of_mixtures.json
│ │ │ │ │ ├── minion_naga_corsair.json
│ │ │ │ │ ├── minion_patches_the_pirate.json
│ │ │ │ │ ├── minion_red_mana_wyrm.json
│ │ │ │ │ ├── minion_second-rate_bruiser.json
│ │ │ │ │ ├── minion_sergeant_sally.json
│ │ │ │ │ ├── minion_small-time_buccaneer.json
│ │ │ │ │ ├── minion_spiked_hogrider.json
│ │ │ │ │ ├── minion_street_trickster.json
│ │ │ │ │ ├── minion_streetwise_investigator.json
│ │ │ │ │ ├── minion_tanaris_hogchopper.json
│ │ │ │ │ ├── minion_toxic_sewer_ooze.json
│ │ │ │ │ ├── minion_weasel_tunneler.json
│ │ │ │ │ ├── minion_wind-up_burglebot.json
│ │ │ │ │ ├── minion_worgen_greaser.json
│ │ │ │ │ ├── minion_wrathion.json
│ │ │ │ │ └── token_ogre.json
│ │ │ │ ├── paladin/
│ │ │ │ │ ├── minion_grimestreet_enforcer.json
│ │ │ │ │ ├── minion_grimestreet_outfitter.json
│ │ │ │ │ ├── minion_grimestreet_protector.json
│ │ │ │ │ ├── minion_grimscale_chum.json
│ │ │ │ │ ├── minion_meanstreet_marshal.json
│ │ │ │ │ ├── minion_wickerflame_burnbirstle.json
│ │ │ │ │ ├── secret_getaway_kodo.json
│ │ │ │ │ ├── spell_small-time_recruits.json
│ │ │ │ │ └── spell_smugglers_run.json
│ │ │ │ ├── priest/
│ │ │ │ │ ├── minion_drakonid_operative.json
│ │ │ │ │ ├── minion_kabal_songstealer.json
│ │ │ │ │ ├── minion_kabal_talonpriest.json
│ │ │ │ │ ├── minion_mana_geode.json
│ │ │ │ │ ├── minion_raza_the_chained.json
│ │ │ │ │ ├── spell_dragonfire_potion.json
│ │ │ │ │ ├── spell_greater_healing_potion.json
│ │ │ │ │ ├── spell_pint-size_potion.json
│ │ │ │ │ ├── spell_potion_of_madness.json
│ │ │ │ │ └── token_crystal.json
│ │ │ │ ├── rogue/
│ │ │ │ │ ├── minion_gadgetzan_ferryman.json
│ │ │ │ │ ├── minion_jade_swarmer.json
│ │ │ │ │ ├── minion_lotus_assassin.json
│ │ │ │ │ ├── minion_luckydo_buccaneer.json
│ │ │ │ │ ├── minion_shadow_rager.json
│ │ │ │ │ ├── minion_shadow_sensei.json
│ │ │ │ │ ├── minion_shaku_the_collector.json
│ │ │ │ │ ├── spell_counterfeit_coin.json
│ │ │ │ │ └── spell_jade_shuriken.json
│ │ │ │ ├── shaman/
│ │ │ │ │ ├── minion_jade_chieftain.json
│ │ │ │ │ ├── minion_jinyu_waterspeaker.json
│ │ │ │ │ ├── minion_lotus_illusionist.json
│ │ │ │ │ ├── minion_white_eyes.json
│ │ │ │ │ ├── spell_call_in_the_finishers.json
│ │ │ │ │ ├── spell_devolve.json
│ │ │ │ │ ├── spell_finders_keepers.json
│ │ │ │ │ ├── spell_jade_lightning.json
│ │ │ │ │ ├── token_murloc_razorgill.json
│ │ │ │ │ ├── token_the_storm_guardian.json
│ │ │ │ │ └── weapon_jade_claws.json
│ │ │ │ ├── warlock/
│ │ │ │ │ ├── minion_abyssal_enforcer.json
│ │ │ │ │ ├── minion_crystalweaver.json
│ │ │ │ │ ├── minion_kabal_trafficker.json
│ │ │ │ │ ├── minion_krul_the_unshackled.json
│ │ │ │ │ ├── minion_seadevil_stinger.json
│ │ │ │ │ ├── minion_unlicensed_apothecary.json
│ │ │ │ │ ├── spell_blastcrystal_potion.json
│ │ │ │ │ ├── spell_bloodfury_potion.json
│ │ │ │ │ └── spell_felfire_potion.json
│ │ │ │ └── warrior/
│ │ │ │ ├── minion_alley_armorsmith.json
│ │ │ │ ├── minion_grimestreet_pawnbroker.json
│ │ │ │ ├── minion_grimy_gadgeteer.json
│ │ │ │ ├── minion_hobart_grapplehammer.json
│ │ │ │ ├── minion_public_defender.json
│ │ │ │ ├── spell_i_know_a_guy.json
│ │ │ │ ├── spell_sleep_with_the_fishes.json
│ │ │ │ ├── spell_stolen_goods.json
│ │ │ │ └── weapon_brass_knuckles.json
│ │ │ ├── naxxramas/
│ │ │ │ ├── minion_anubar_ambusher.json
│ │ │ │ ├── minion_baron_rivendare.json
│ │ │ │ ├── minion_dancing_swords.json
│ │ │ │ ├── minion_dark_cultist.json
│ │ │ │ ├── minion_deathlord.json
│ │ │ │ ├── minion_echoing_ooze.json
│ │ │ │ ├── minion_feugen.json
│ │ │ │ ├── minion_haunted_creeper.json
│ │ │ │ ├── minion_kelthuzad.json
│ │ │ │ ├── minion_loatheb.json
│ │ │ │ ├── minion_mad_scientist.json
│ │ │ │ ├── minion_maexxna.json
│ │ │ │ ├── minion_nerubar_weblord.json
│ │ │ │ ├── minion_nerubian_egg.json
│ │ │ │ ├── minion_shade_of_naxxramas.json
│ │ │ │ ├── minion_sludge_belcher.json
│ │ │ │ ├── minion_spectral_knight.json
│ │ │ │ ├── minion_stalagg.json
│ │ │ │ ├── minion_stoneskin_gargoyle.json
│ │ │ │ ├── minion_undertaker.json
│ │ │ │ ├── minion_unstable_ghoul.json
│ │ │ │ ├── minion_voidcaller.json
│ │ │ │ ├── minion_wailing_soul.json
│ │ │ │ ├── minion_webspinner.json
│ │ │ │ ├── minion_zombie_chow.json
│ │ │ │ ├── secret_avenge.json
│ │ │ │ ├── secret_duplicate.json
│ │ │ │ ├── spell_poison_seeds.json
│ │ │ │ ├── spell_reincarnate.json
│ │ │ │ ├── token_nerubian.json
│ │ │ │ ├── token_slime.json
│ │ │ │ ├── token_spectral_spider.json
│ │ │ │ ├── token_thaddius.json
│ │ │ │ └── weapon_deaths_bite.json
│ │ │ ├── one_night_in_karazhan/
│ │ │ │ ├── minion_arcane_anomaly.json
│ │ │ │ ├── minion_arcane_giant.json
│ │ │ │ ├── minion_arcanosmith.json
│ │ │ │ ├── minion_avian_watcher.json
│ │ │ │ ├── minion_babbling_book.json
│ │ │ │ ├── minion_barnes.json
│ │ │ │ ├── minion_book_wyrm.json
│ │ │ │ ├── minion_cloaked_huntress.json
│ │ │ │ ├── minion_deadly_fork.json
│ │ │ │ ├── minion_enchanted_raven.json
│ │ │ │ ├── minion_ethereal_peddler.json
│ │ │ │ ├── minion_ivory_knight.json
│ │ │ │ ├── minion_kindly_grandmother.json
│ │ │ │ ├── minion_malchezaars_imp.json
│ │ │ │ ├── minion_medivh_the_guardian.json
│ │ │ │ ├── minion_medivhs_valet.json
│ │ │ │ ├── minion_menagerie_magician.json
│ │ │ │ ├── minion_menagerie_warden.json
│ │ │ │ ├── minion_moat_lurker.json
│ │ │ │ ├── minion_moroes.json
│ │ │ │ ├── minion_netherspite_historian.json
│ │ │ │ ├── minion_nightbane_templar.json
│ │ │ │ ├── minion_onyx_bishop.json
│ │ │ │ ├── minion_pantry_spider.json
│ │ │ │ ├── minion_pompous_thespian.json
│ │ │ │ ├── minion_priest_of_the_feast.json
│ │ │ │ ├── minion_prince_malchezaar.json
│ │ │ │ ├── minion_runic_egg.json
│ │ │ │ ├── minion_silverware_golem.json
│ │ │ │ ├── minion_swashburglar.json
│ │ │ │ ├── minion_the_curator.json
│ │ │ │ ├── minion_violet_illusionist.json
│ │ │ │ ├── minion_wicked_witchdoctor.json
│ │ │ │ ├── minion_zoobot.json
│ │ │ │ ├── secret_cat_trick.json
│ │ │ │ ├── spell_firelands_portal.json
│ │ │ │ ├── spell_ironforge_portal.json
│ │ │ │ ├── spell_kara_kazham.json
│ │ │ │ ├── spell_maelstrom_portal.json
│ │ │ │ ├── spell_moonglade_portal.json
│ │ │ │ ├── spell_protect_the_king.json
│ │ │ │ ├── spell_purify.json
│ │ │ │ ├── spell_silvermoon_portal.json
│ │ │ │ ├── token_animated_shield.json
│ │ │ │ ├── token_big_bad_wolf.json
│ │ │ │ ├── token_broom.json
│ │ │ │ ├── token_candle.json
│ │ │ │ ├── token_cat_in_a_hat.json
│ │ │ │ ├── token_cellar_spider.json
│ │ │ │ ├── token_pawn.json
│ │ │ │ ├── token_steward.json
│ │ │ │ ├── token_teapot.json
│ │ │ │ ├── weapon_atiesh.json
│ │ │ │ ├── weapon_fools_bane.json
│ │ │ │ ├── weapon_sharp_fork.json
│ │ │ │ └── weapon_spirit_claws.json
│ │ │ ├── promo/
│ │ │ │ ├── spell_i_am_murloc.json
│ │ │ │ ├── spell_power_of_the_horde.json
│ │ │ │ ├── spell_rogues_do_it.json
│ │ │ │ ├── token_emboldener_3000.json
│ │ │ │ ├── token_homing_chicken.json
│ │ │ │ ├── token_poultryizer.json
│ │ │ │ └── token_repair_bot.json
│ │ │ ├── the_grand_tournament/
│ │ │ │ ├── druid/
│ │ │ │ │ ├── hero_power_dire_shapeshift.json
│ │ │ │ │ ├── minion_aviana.json
│ │ │ │ │ ├── minion_darnassus_aspirant.json
│ │ │ │ │ ├── minion_druid_of_the_saber.json
│ │ │ │ │ ├── minion_knight_of_the_wild.json
│ │ │ │ │ ├── minion_savage_combatant.json
│ │ │ │ │ ├── minion_wildwalker.json
│ │ │ │ │ ├── spell_astral_communion.json
│ │ │ │ │ ├── spell_living_roots.json
│ │ │ │ │ ├── spell_living_roots_1.json
│ │ │ │ │ ├── spell_living_roots_2.json
│ │ │ │ │ ├── spell_living_roots_3.json
│ │ │ │ │ ├── spell_mulch.json
│ │ │ │ │ ├── token_sabertooth_lion.json
│ │ │ │ │ ├── token_sabertooth_panther.json
│ │ │ │ │ ├── token_sabertooth_tiger.json
│ │ │ │ │ └── token_sapling.json
│ │ │ │ ├── hunter/
│ │ │ │ │ ├── hero_power_ballista_shot.json
│ │ │ │ │ ├── minion_acidmaw.json
│ │ │ │ │ ├── minion_brave_archer.json
│ │ │ │ │ ├── minion_dreadscale.json
│ │ │ │ │ ├── minion_kings_elekk.json
│ │ │ │ │ ├── minion_ram_wrangler.json
│ │ │ │ │ ├── minion_stablemaster.json
│ │ │ │ │ ├── secret_bear_trap.json
│ │ │ │ │ ├── spell_ball_of_spiders.json
│ │ │ │ │ ├── spell_lock_and_load.json
│ │ │ │ │ └── spell_powershot.json
│ │ │ │ ├── mage/
│ │ │ │ │ ├── hero_power_fireblast_rank_2.json
│ │ │ │ │ ├── minion_coldarra_drake.json
│ │ │ │ │ ├── minion_dalaran_aspirant.json
│ │ │ │ │ ├── minion_fallen_hero.json
│ │ │ │ │ ├── minion_rhonin.json
│ │ │ │ │ ├── minion_spellslinger.json
│ │ │ │ │ ├── secret_effigy.json
│ │ │ │ │ ├── spell_arcane_blast.json
│ │ │ │ │ ├── spell_flame_lance.json
│ │ │ │ │ ├── spell_polymorph_boar.json
│ │ │ │ │ └── token_mage_huffer.json
│ │ │ │ ├── neutral/
│ │ │ │ │ ├── minion_argent_horserider.json
│ │ │ │ │ ├── minion_argent_watchman.json
│ │ │ │ │ ├── minion_armored_warhorse.json
│ │ │ │ │ ├── minion_bolf_ramshield.json
│ │ │ │ │ ├── minion_boneguard_lieutenant.json
│ │ │ │ │ ├── minion_captured_jormungar.json
│ │ │ │ │ ├── minion_chillmaw.json
│ │ │ │ │ ├── minion_clockwork_knight.json
│ │ │ │ │ ├── minion_coliseum_manager.json
│ │ │ │ │ ├── minion_crowd_favorite.json
│ │ │ │ │ ├── minion_dragonhawk_rider.json
│ │ │ │ │ ├── minion_evil_heckler.json
│ │ │ │ │ ├── minion_eydis_darkbane.json
│ │ │ │ │ ├── minion_fencing_coach.json
│ │ │ │ │ ├── minion_fjola_lightbane.json
│ │ │ │ │ ├── minion_flame_juggler.json
│ │ │ │ │ ├── minion_frigid_snobold.json
│ │ │ │ │ ├── minion_frost_giant.json
│ │ │ │ │ ├── minion_gadgetzan_jouster.json
│ │ │ │ │ ├── minion_garrison_commander.json
│ │ │ │ │ ├── minion_gormok_the_impaler.json
│ │ │ │ │ ├── minion_grand_crusader.json
│ │ │ │ │ ├── minion_ice_rager.json
│ │ │ │ │ ├── minion_icehowl.json
│ │ │ │ │ ├── minion_injured_kvaldir.json
│ │ │ │ │ ├── minion_justicar_trueheart.json
│ │ │ │ │ ├── minion_kodorider.json
│ │ │ │ │ ├── minion_kvaldir_raider.json
│ │ │ │ │ ├── minion_lance_carrier.json
│ │ │ │ │ ├── minion_lights_champion.json
│ │ │ │ │ ├── minion_lowly_squire.json
│ │ │ │ │ ├── minion_maiden_of_the_lake.json
│ │ │ │ │ ├── minion_master_jouster.json
│ │ │ │ │ ├── minion_master_of_ceremonies.json
│ │ │ │ │ ├── minion_mogors_champion.json
│ │ │ │ │ ├── minion_muklas_champion.json
│ │ │ │ │ ├── minion_nexus-champion_saraad.json
│ │ │ │ │ ├── minion_north_sea_kraken.json
│ │ │ │ │ ├── minion_pit_fighter.json
│ │ │ │ │ ├── minion_recruiter.json
│ │ │ │ │ ├── minion_refreshment_vendor.json
│ │ │ │ │ ├── minion_saboteur.json
│ │ │ │ │ ├── minion_sideshow_spelleater.json
│ │ │ │ │ ├── minion_silent_knight.json
│ │ │ │ │ ├── minion_silver_hand_regent.json
│ │ │ │ │ ├── minion_skycapn_kragg.json
│ │ │ │ │ ├── minion_the_skeleton_knight.json
│ │ │ │ │ ├── minion_tournament_attendee.json
│ │ │ │ │ ├── minion_tournament_medic.json
│ │ │ │ │ ├── minion_twilight_guardian.json
│ │ │ │ │ └── token_war_kodo.json
│ │ │ │ ├── paladin/
│ │ │ │ │ ├── hero_power_the_silver_hand.json
│ │ │ │ │ ├── minion_eadric_the_pure.json
│ │ │ │ │ ├── minion_murloc_knight.json
│ │ │ │ │ ├── minion_mysterious_challenger.json
│ │ │ │ │ ├── minion_tuskarr_jouster.json
│ │ │ │ │ ├── minion_warhorse_trainer.json
│ │ │ │ │ ├── secret_competitive_spirit.json
│ │ │ │ │ ├── spell_enter_the_coliseum.json
│ │ │ │ │ ├── spell_seal_of_champions.json
│ │ │ │ │ └── weapon_argent_lance.json
│ │ │ │ ├── priest/
│ │ │ │ │ ├── hero_power_heal.json
│ │ │ │ │ ├── minion_confessor_paletress.json
│ │ │ │ │ ├── minion_holy_champion.json
│ │ │ │ │ ├── minion_shadowfiend.json
│ │ │ │ │ ├── minion_spawn_of_shadows.json
│ │ │ │ │ ├── minion_wyrmrest_agent.json
│ │ │ │ │ ├── spell_confuse.json
│ │ │ │ │ ├── spell_convert.json
│ │ │ │ │ ├── spell_flash_heal.json
│ │ │ │ │ └── spell_power_word_glory.json
│ │ │ │ ├── rogue/
│ │ │ │ │ ├── hero_power_poisoned_dagger.json
│ │ │ │ │ ├── minion_anubarak.json
│ │ │ │ │ ├── minion_buccaneer.json
│ │ │ │ │ ├── minion_cutpurse.json
│ │ │ │ │ ├── minion_shado-pan_rider.json
│ │ │ │ │ ├── minion_shady_dealer.json
│ │ │ │ │ ├── minion_undercity_valiant.json
│ │ │ │ │ ├── spell_ambush.json
│ │ │ │ │ ├── spell_beneath_the_ground.json
│ │ │ │ │ ├── spell_burgle.json
│ │ │ │ │ ├── weapon_poisoned_blade.json
│ │ │ │ │ └── weapon_poisoned_dagger.json
│ │ │ │ ├── shaman/
│ │ │ │ │ ├── hero_power_lightning_jolt.json
│ │ │ │ │ ├── hero_power_totemic_slam.json
│ │ │ │ │ ├── minion_draenei_totemcarver.json
│ │ │ │ │ ├── minion_the_mistcaller.json
│ │ │ │ │ ├── minion_thunder_bluff_valiant.json
│ │ │ │ │ ├── minion_totem_golem.json
│ │ │ │ │ ├── minion_tuskarr_totemic.json
│ │ │ │ │ ├── spell_ancestral_knowledge.json
│ │ │ │ │ ├── spell_elemental_destruction.json
│ │ │ │ │ ├── spell_healing_wave.json
│ │ │ │ │ ├── spell_summon_healing_totem.json
│ │ │ │ │ ├── spell_summon_searing_totem.json
│ │ │ │ │ ├── spell_summon_stoneclaw_totem.json
│ │ │ │ │ ├── spell_summon_wrath_of_air_totem.json
│ │ │ │ │ └── weapon_charged_hammer.json
│ │ │ │ ├── warlock/
│ │ │ │ │ ├── hero_power_soul_tap.json
│ │ │ │ │ ├── minion_dreadsteed.json
│ │ │ │ │ ├── minion_fearsome_doomguard.json
│ │ │ │ │ ├── minion_tiny_knight_of_evil.json
│ │ │ │ │ ├── minion_void_crusher.json
│ │ │ │ │ ├── minion_wilfred_fizzlebang.json
│ │ │ │ │ ├── minion_wrathguard.json
│ │ │ │ │ ├── spell_dark_bargain.json
│ │ │ │ │ ├── spell_demonfuse.json
│ │ │ │ │ └── spell_fist_of_jaraxxus.json
│ │ │ │ └── warrior/
│ │ │ │ ├── hero_power_tank_up.json
│ │ │ │ ├── minion_alexstraszas_champion.json
│ │ │ │ ├── minion_magnataur_alpha.json
│ │ │ │ ├── minion_orgrimmar_aspirant.json
│ │ │ │ ├── minion_sea_reaver.json
│ │ │ │ ├── minion_sparring_partner.json
│ │ │ │ ├── minion_varian_wrynn.json
│ │ │ │ ├── spell_bash.json
│ │ │ │ ├── spell_bolster.json
│ │ │ │ └── weapon_kings_defender.json
│ │ │ └── the_old_gods/
│ │ │ ├── druid/
│ │ │ │ ├── minion_addled_grizzly.json
│ │ │ │ ├── minion_dark_arakkoa.json
│ │ │ │ ├── minion_fandral_staghelm.json
│ │ │ │ ├── minion_forbidden_ancient.json
│ │ │ │ ├── minion_klaxxi_amber-weaver.json
│ │ │ │ ├── minion_mire_keeper.json
│ │ │ │ ├── spell_feral_rage.json
│ │ │ │ ├── spell_feral_rage_1.json
│ │ │ │ ├── spell_feral_rage_2.json
│ │ │ │ ├── spell_feral_rage_3.json
│ │ │ │ ├── spell_mark_of_yshaarj.json
│ │ │ │ ├── spell_wisps_of_the_old_gods.json
│ │ │ │ ├── spell_wisps_of_the_old_gods_1.json
│ │ │ │ ├── spell_wisps_of_the_old_gods_2.json
│ │ │ │ └── spell_wisps_of_the_old_gods_3.json
│ │ │ ├── hunter/
│ │ │ │ ├── minion_carrion_grub.json
│ │ │ │ ├── minion_fiery_bat.json
│ │ │ │ ├── minion_forlorn_stalker.json
│ │ │ │ ├── minion_giant_sand_worm.json
│ │ │ │ ├── minion_infested_wolf.json
│ │ │ │ ├── minion_princess_huhuran.json
│ │ │ │ ├── spell_call_of_the_wild.json
│ │ │ │ ├── spell_infest.json
│ │ │ │ ├── spell_on_the_hunt.json
│ │ │ │ ├── token_mastiff.json
│ │ │ │ └── token_spider.json
│ │ │ ├── mage/
│ │ │ │ ├── minion_anomalus.json
│ │ │ │ ├── minion_cult_sorcerer.json
│ │ │ │ ├── minion_demented_frostcaller.json
│ │ │ │ ├── minion_faceless_summoner.json
│ │ │ │ ├── minion_servant_of_yogg_saron.json
│ │ │ │ ├── minion_twilight_flamecaller.json
│ │ │ │ ├── spell_cabalists_tome.json
│ │ │ │ ├── spell_forbidden_flame.json
│ │ │ │ └── spell_shatter.json
│ │ │ ├── neutral/
│ │ │ │ ├── minion_aberrant_berserker.json
│ │ │ │ ├── minion_amgam_rager.json
│ │ │ │ ├── minion_ancient_harbinger.json
│ │ │ │ ├── minion_beckoner_of_evil.json
│ │ │ │ ├── minion_bilefin_tidehunter.json
│ │ │ │ ├── minion_blackwater_pirate.json
│ │ │ │ ├── minion_blood_of_the_ancient_one.json
│ │ │ │ ├── minion_bog_creeper.json
│ │ │ │ ├── minion_corrupted_healbot.json
│ │ │ │ ├── minion_corrupted_seer.json
│ │ │ │ ├── minion_crazed_worshipper.json
│ │ │ │ ├── minion_cthun.json
│ │ │ │ ├── minion_cthuns_chosen.json
│ │ │ │ ├── minion_cult_apothecary.json
│ │ │ │ ├── minion_cyclopian_horror.json
│ │ │ │ ├── minion_darkspeaker.json
│ │ │ │ ├── minion_deathwing_dragonlord.json
│ │ │ │ ├── minion_disciple_of_cthun.json
│ │ │ │ ├── minion_doomcaller.json
│ │ │ │ ├── minion_duskboar.json
│ │ │ │ ├── minion_eater_of_secrets.json
│ │ │ │ ├── minion_eldritch_horror.json
│ │ │ │ ├── minion_evolved_kobold.json
│ │ │ │ ├── minion_faceless_behemoth.json
│ │ │ │ ├── minion_faceless_shambler.json
│ │ │ │ ├── minion_grotesque_dragonhawk.json
│ │ │ │ ├── minion_hogger_doom_of_elwynn.json
│ │ │ │ ├── minion_infested_tauren.json
│ │ │ │ ├── minion_midnight_drake.json
│ │ │ │ ├── minion_mukla_tyrant_of_the_vale.json
│ │ │ │ ├── minion_nat_the_darkfisher.json
│ │ │ │ ├── minion_nerubian_prophet.json
│ │ │ │ ├── minion_nzoth_the_corruptor.json
│ │ │ │ ├── minion_polluted_hoarder.json
│ │ │ │ ├── minion_psych-o-tron.json
│ │ │ │ ├── minion_scaled_nightmare.json
│ │ │ │ ├── minion_shifter_zerus.json
│ │ │ │ ├── minion_silithid_swarmer.json
│ │ │ │ ├── minion_skeram_cultist.json
│ │ │ │ ├── minion_soggoth_the_slitherer.json
│ │ │ │ ├── minion_spawn_of_nzoth.json
│ │ │ │ ├── minion_squirming_tentacle.json
│ │ │ │ ├── minion_tentacle_of_nzoth.json
│ │ │ │ ├── minion_the_boogeymonster.json
│ │ │ │ ├── minion_twilight_elder.json
│ │ │ │ ├── minion_twilight_geomancer.json
│ │ │ │ ├── minion_twilight_summoner.json
│ │ │ │ ├── minion_twin_emperor_veklor.json
│ │ │ │ ├── minion_twisted_worgen.json
│ │ │ │ ├── minion_validated_doomsayer.json
│ │ │ │ ├── minion_yogg_saron_hopes_end.json
│ │ │ │ ├── minion_yshaarj_rage_unbound.json
│ │ │ │ ├── minion_zealous_initiate.json
│ │ │ │ ├── token_faceless_destroyer.json
│ │ │ │ ├── token_ooze.json
│ │ │ │ ├── token_tauren_slime.json
│ │ │ │ ├── token_the_ancient_one.json
│ │ │ │ └── token_twin_emperor_veknilash.json
│ │ │ ├── paladin/
│ │ │ │ ├── hero_power_the_tidal_hand.json
│ │ │ │ ├── minion_ragnaros_lightlord.json
│ │ │ │ ├── minion_selfless_hero.json
│ │ │ │ ├── minion_steward_of_darkshire.json
│ │ │ │ ├── minion_vilefin_inquisitor.json
│ │ │ │ ├── spell_a_light_in_the_darkness.json
│ │ │ │ ├── spell_divine_strength.json
│ │ │ │ ├── spell_forbidden_healing.json
│ │ │ │ ├── spell_stand_against_darkness.json
│ │ │ │ ├── token_silver_hand_murloc.json
│ │ │ │ └── weapon_rallying_blade.json
│ │ │ ├── priest/
│ │ │ │ ├── minion_darkshire_alchemist.json
│ │ │ │ ├── minion_herald_volazj.json
│ │ │ │ ├── minion_hooded_acolyte.json
│ │ │ │ ├── minion_shifting_shade.json
│ │ │ │ ├── minion_twilight_darkmender.json
│ │ │ │ ├── spell_embrace_the_shadow.json
│ │ │ │ ├── spell_forbidden_shaping.json
│ │ │ │ ├── spell_power_word_tentacles.json
│ │ │ │ └── spell_shadow_word_horror.json
│ │ │ ├── rogue/
│ │ │ │ ├── minion_blade_of_cthun.json
│ │ │ │ ├── minion_bladed_cultist.json
│ │ │ │ ├── minion_shadowcaster.json
│ │ │ │ ├── minion_southsea_squidface.json
│ │ │ │ ├── minion_undercity_huckster.json
│ │ │ │ ├── minion_xaril_poisoned_mind.json
│ │ │ │ ├── spell_bloodthistle_toxin.json
│ │ │ │ ├── spell_briarthorn_toxin.json
│ │ │ │ ├── spell_fadeleaf_toxin.json
│ │ │ │ ├── spell_firebloom_toxin.json
│ │ │ │ ├── spell_journey_below.json
│ │ │ │ ├── spell_kingsblood_toxin.json
│ │ │ │ ├── spell_shadow_strike.json
│ │ │ │ └── spell_thistle_tea.json
│ │ │ ├── shaman/
│ │ │ │ ├── minion_eternal_sentinel.json
│ │ │ │ ├── minion_flamewreathed_faceless.json
│ │ │ │ ├── minion_hallazeal_the_ascended.json
│ │ │ │ ├── minion_master_of_evolution.json
│ │ │ │ ├── minion_thing_from_below.json
│ │ │ │ ├── spell_evolve.json
│ │ │ │ ├── spell_primal_fusion.json
│ │ │ │ ├── spell_stormcrack.json
│ │ │ │ ├── token_twilight_elemental.json
│ │ │ │ └── weapon_hammer_of_twilight.json
│ │ │ ├── warlock/
│ │ │ │ ├── minion_chogall.json
│ │ │ │ ├── minion_darkshire_councilman.json
│ │ │ │ ├── minion_darkshire_librarian.json
│ │ │ │ ├── minion_possessed_villager.json
│ │ │ │ ├── minion_usher_of_souls.json
│ │ │ │ ├── spell_doom.json
│ │ │ │ ├── spell_forbidden_ritual.json
│ │ │ │ ├── spell_renounce_darkness.json
│ │ │ │ ├── spell_spreading_madness.json
│ │ │ │ ├── token_icky_tentacle.json
│ │ │ │ └── token_shadowbeast.json
│ │ │ └── warrior/
│ │ │ ├── minion_ancient_shieldbearer.json
│ │ │ ├── minion_bloodhoof_brave.json
│ │ │ ├── minion_bloodsail_cultist.json
│ │ │ ├── minion_malkorok.json
│ │ │ ├── minion_nzoths_first_mate.json
│ │ │ ├── minion_ravaging_ghoul.json
│ │ │ ├── spell_blood_to_ichor.json
│ │ │ ├── spell_blood_warriors.json
│ │ │ ├── weapon_rusty_hook.json
│ │ │ └── weapon_tentacles_for_arms.json
│ │ ├── decks/
│ │ │ ├── aggro_shaman.json
│ │ │ ├── aggrodin.json
│ │ │ ├── beastrattle_hunter.json
│ │ │ ├── burgle_rogue.json
│ │ │ ├── face_hunter.json
│ │ │ ├── freeze_mage.json
│ │ │ ├── jade_druid.json
│ │ │ ├── jade_miracle_druid.json
│ │ │ ├── jade_rogue.json
│ │ │ ├── midrange_shaman.json
│ │ │ ├── miracle_rogue.json
│ │ │ ├── pirate_warrior.json
│ │ │ ├── reno_mage.json
│ │ │ ├── reno_priest.json
│ │ │ ├── renolock.json
│ │ │ └── wild_pirate_warrior.json
│ │ ├── formats/
│ │ │ ├── all.json
│ │ │ ├── standard.json
│ │ │ └── wild.json
│ │ └── training/
│ │ ├── budeget_effective_gvg_rogue_tempo_mech_synergy.json
│ │ ├── gvg_face_hunter_season_9_legend_24_na.json
│ │ └── handlock_mechanization_____.json
│ └── test/
│ └── java/
│ └── net/
│ └── demilich/
│ └── metastone/
│ └── tests/
│ └── ValidateCards.java
├── documentation/
│ ├── attributes.txt
│ ├── card.txt
│ ├── conditions.txt
│ ├── filters.txt
│ ├── knowledge.txt
│ ├── known_issues.txt
│ ├── spells.txt
│ ├── triggers.txt
│ └── valueproviders.txt
├── game/
│ ├── build.gradle
│ ├── lib/
│ │ └── jsoup-1.10.2.jar
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── net/
│ │ └── demilich/
│ │ └── metastone/
│ │ └── game/
│ │ ├── Attribute.java
│ │ ├── Environment.java
│ │ ├── GameContext.java
│ │ ├── Player.java
│ │ ├── PlayerAttribute.java
│ │ ├── TurnState.java
│ │ ├── actions/
│ │ │ ├── ActionType.java
│ │ │ ├── BattlecryAction.java
│ │ │ ├── DiscoverAction.java
│ │ │ ├── EndTurnAction.java
│ │ │ ├── GameAction.java
│ │ │ ├── HeroPowerAction.java
│ │ │ ├── IActionSelectionListener.java
│ │ │ ├── IBattlecryCondition.java
│ │ │ ├── PhysicalAttackAction.java
│ │ │ ├── PlayCardAction.java
│ │ │ ├── PlayChooseOneCardAction.java
│ │ │ ├── PlayMinionCardAction.java
│ │ │ ├── PlayPermanentCardAction.java
│ │ │ ├── PlaySpellCardAction.java
│ │ │ └── PlayWeaponCardAction.java
│ │ ├── behaviour/
│ │ │ ├── Behaviour.java
│ │ │ ├── DoNothingBehaviour.java
│ │ │ ├── FlatMonteCarlo.java
│ │ │ ├── GreedyOptimizeMove.java
│ │ │ ├── GreedyOptimizeTurn.java
│ │ │ ├── IBehaviour.java
│ │ │ ├── NoAggressionBehaviour.java
│ │ │ ├── PlayRandomBehaviour.java
│ │ │ ├── TranspositionTable.java
│ │ │ ├── heuristic/
│ │ │ │ ├── IGameStateHeuristic.java
│ │ │ │ ├── WeightedFeature.java
│ │ │ │ └── WeightedHeuristic.java
│ │ │ ├── human/
│ │ │ │ ├── ActionGroup.java
│ │ │ │ ├── HumanActionOptions.java
│ │ │ │ ├── HumanBehaviour.java
│ │ │ │ ├── HumanMulliganOptions.java
│ │ │ │ └── HumanTargetOptions.java
│ │ │ ├── learning/
│ │ │ │ ├── Brain.java
│ │ │ │ ├── IBrain.java
│ │ │ │ └── LearningBehaviour.java
│ │ │ ├── mcts/
│ │ │ │ ├── ITreePolicy.java
│ │ │ │ ├── MonteCarloTreeSearch.java
│ │ │ │ ├── Node.java
│ │ │ │ └── UctPolicy.java
│ │ │ ├── neutralnetwork/
│ │ │ │ ├── HiddenUnit.java
│ │ │ │ ├── InputUnit.java
│ │ │ │ ├── NeuralNetwork.java
│ │ │ │ └── Unit.java
│ │ │ └── threat/
│ │ │ ├── GameStateValueBehaviour.java
│ │ │ ├── ThreatBasedHeuristic.java
│ │ │ ├── ThreatLevel.java
│ │ │ └── cuckoo/
│ │ │ ├── CuckooAgent.java
│ │ │ ├── CuckooLearner.java
│ │ │ ├── IFitnessFunction.java
│ │ │ └── WinRateFitness.java
│ │ ├── cards/
│ │ │ ├── Card.java
│ │ │ ├── CardCatalogue.java
│ │ │ ├── CardCollection.java
│ │ │ ├── CardDescType.java
│ │ │ ├── CardParseException.java
│ │ │ ├── CardParser.java
│ │ │ ├── CardSet.java
│ │ │ ├── CardType.java
│ │ │ ├── ChooseBattlecryCard.java
│ │ │ ├── ChooseOneCard.java
│ │ │ ├── HeroCard.java
│ │ │ ├── IChooseOneCard.java
│ │ │ ├── MinionCard.java
│ │ │ ├── PermanentCard.java
│ │ │ ├── QuestCard.java
│ │ │ ├── Rarity.java
│ │ │ ├── SecretCard.java
│ │ │ ├── SpellCard.java
│ │ │ ├── SummonCard.java
│ │ │ ├── WeaponCard.java
│ │ │ ├── costmodifier/
│ │ │ │ ├── CardCostModifier.java
│ │ │ │ ├── OneTurnCostModifier.java
│ │ │ │ └── ToggleCostModifier.java
│ │ │ └── desc/
│ │ │ ├── ActorCardDesc.java
│ │ │ ├── AttributeDeserializer.java
│ │ │ ├── AuraDeserializer.java
│ │ │ ├── CardCostModifierDeserializer.java
│ │ │ ├── CardDesc.java
│ │ │ ├── ChooseBattlecryCardDesc.java
│ │ │ ├── ChooseOneCardDesc.java
│ │ │ ├── ConditionDeserializer.java
│ │ │ ├── Desc.java
│ │ │ ├── FilterDeserializer.java
│ │ │ ├── HeroCardDesc.java
│ │ │ ├── HeroPowerCardDesc.java
│ │ │ ├── MinionCardDesc.java
│ │ │ ├── ParseUtils.java
│ │ │ ├── ParseValueType.java
│ │ │ ├── PermanentCardDesc.java
│ │ │ ├── QuestCardDesc.java
│ │ │ ├── SecretCardDesc.java
│ │ │ ├── SourceDeserializer.java
│ │ │ ├── SpellCardDesc.java
│ │ │ ├── SpellDeserializer.java
│ │ │ ├── SummonCardDesc.java
│ │ │ ├── ValueProviderDeserializer.java
│ │ │ └── WeaponCardDesc.java
│ │ ├── decks/
│ │ │ ├── Deck.java
│ │ │ ├── DeckFactory.java
│ │ │ ├── DeckFormat.java
│ │ │ ├── MetaDeck.java
│ │ │ ├── RandomDeck.java
│ │ │ └── validation/
│ │ │ ├── ArbitraryDeckValidator.java
│ │ │ ├── DefaultDeckValidator.java
│ │ │ └── IDeckValidator.java
│ │ ├── entities/
│ │ │ ├── Actor.java
│ │ │ ├── Entity.java
│ │ │ ├── EntityType.java
│ │ │ ├── heroes/
│ │ │ │ ├── Hero.java
│ │ │ │ ├── HeroClass.java
│ │ │ │ └── MetaHero.java
│ │ │ ├── minions/
│ │ │ │ ├── Minion.java
│ │ │ │ ├── Permanent.java
│ │ │ │ ├── Race.java
│ │ │ │ ├── RelativeToSource.java
│ │ │ │ └── Summon.java
│ │ │ └── weapons/
│ │ │ └── Weapon.java
│ │ ├── events/
│ │ │ ├── AfterPhysicalAttackEvent.java
│ │ │ ├── AfterSpellCastedEvent.java
│ │ │ ├── AfterSummonEvent.java
│ │ │ ├── ArmorGainedEvent.java
│ │ │ ├── BeforeSummonEvent.java
│ │ │ ├── BoardChangedEvent.java
│ │ │ ├── CardPlayedEvent.java
│ │ │ ├── CardRevealedEvent.java
│ │ │ ├── DamageEvent.java
│ │ │ ├── DiscardEvent.java
│ │ │ ├── DrawCardEvent.java
│ │ │ ├── EnrageChangedEvent.java
│ │ │ ├── GameEvent.java
│ │ │ ├── GameEventType.java
│ │ │ ├── GameStartEvent.java
│ │ │ ├── HealEvent.java
│ │ │ ├── HeroPowerUsedEvent.java
│ │ │ ├── JoustEvent.java
│ │ │ ├── KillEvent.java
│ │ │ ├── OverloadEvent.java
│ │ │ ├── PhysicalAttackEvent.java
│ │ │ ├── PreDamageEvent.java
│ │ │ ├── QuestPlayedEvent.java
│ │ │ ├── QuestSuccessfulEvent.java
│ │ │ ├── SecretPlayedEvent.java
│ │ │ ├── SecretRevealedEvent.java
│ │ │ ├── SilenceEvent.java
│ │ │ ├── SpellCastedEvent.java
│ │ │ ├── SummonEvent.java
│ │ │ ├── TargetAcquisitionEvent.java
│ │ │ ├── TurnEndEvent.java
│ │ │ ├── TurnStartEvent.java
│ │ │ ├── WeaponDestroyedEvent.java
│ │ │ └── WeaponEquippedEvent.java
│ │ ├── gameconfig/
│ │ │ ├── GameConfig.java
│ │ │ └── PlayerConfig.java
│ │ ├── heroes/
│ │ │ └── powers/
│ │ │ ├── HeroPower.java
│ │ │ └── HeroPowerChooseOne.java
│ │ ├── logic/
│ │ │ ├── ActionLogic.java
│ │ │ ├── CustomCloneable.java
│ │ │ ├── GameLogic.java
│ │ │ ├── MatchResult.java
│ │ │ └── TargetLogic.java
│ │ ├── spells/
│ │ │ ├── AddAttributeSpell.java
│ │ │ ├── AddDeathrattleSpell.java
│ │ │ ├── AddQuestSpell.java
│ │ │ ├── AddSecretSpell.java
│ │ │ ├── AddSpellTriggerSpell.java
│ │ │ ├── AdjacentEffectSpell.java
│ │ │ ├── AuraBuffSpell.java
│ │ │ ├── BuffHeroSpell.java
│ │ │ ├── BuffSpell.java
│ │ │ ├── BuffWeaponSpell.java
│ │ │ ├── CardCostModifierSpell.java
│ │ │ ├── CastRandomSpellSpell.java
│ │ │ ├── CastRepeatedlySpell.java
│ │ │ ├── ChangeHeroPowerSpell.java
│ │ │ ├── ChangeHeroSpell.java
│ │ │ ├── ClearOverloadSpell.java
│ │ │ ├── CloneMinionSpell.java
│ │ │ ├── ComboSpell.java
│ │ │ ├── ConditionalAttackBonusSpell.java
│ │ │ ├── ConditionalEffectSpell.java
│ │ │ ├── ConditionalSpell.java
│ │ │ ├── CopyCardSpell.java
│ │ │ ├── CopyDeathrattleSpell.java
│ │ │ ├── CopyHeroPower.java
│ │ │ ├── CreateCardSpell.java
│ │ │ ├── CreateSummonSpell.java
│ │ │ ├── DamageSpell.java
│ │ │ ├── DestroyAllExceptOneSpell.java
│ │ │ ├── DestroySecretsSpell.java
│ │ │ ├── DestroySpell.java
│ │ │ ├── DiscardCardsFromDeckSpell.java
│ │ │ ├── DiscardSpell.java
│ │ │ ├── DiscoverCardSpell.java
│ │ │ ├── DiscoverDrawSpell.java
│ │ │ ├── DiscoverFilteredCardSpell.java
│ │ │ ├── DiscoverOptionSpell.java
│ │ │ ├── DiscoverRandomCardSpell.java
│ │ │ ├── DoubleAttackSpell.java
│ │ │ ├── DrawCardAndDoSomethingSpell.java
│ │ │ ├── DrawCardSpell.java
│ │ │ ├── DrawCardUntilConditionSpell.java
│ │ │ ├── EitherOrSpell.java
│ │ │ ├── EnrageSpell.java
│ │ │ ├── EquipRandomWeaponSpell.java
│ │ │ ├── EquipWeaponSpell.java
│ │ │ ├── ForceDeathPhaseSpell.java
│ │ │ ├── FromDeckToHandSpell.java
│ │ │ ├── FumbleSpell.java
│ │ │ ├── GainManaSpell.java
│ │ │ ├── HealSpell.java
│ │ │ ├── ICardPostProcessor.java
│ │ │ ├── ICardProvider.java
│ │ │ ├── JoustSpell.java
│ │ │ ├── MetaSpell.java
│ │ │ ├── MindControlSpell.java
│ │ │ ├── MisdirectSpell.java
│ │ │ ├── MissilesSpell.java
│ │ │ ├── ModifyAttributeSpell.java
│ │ │ ├── ModifyDamageSpell.java
│ │ │ ├── ModifyDurabilitySpell.java
│ │ │ ├── ModifyMaxManaSpell.java
│ │ │ ├── MultiTargetSpell.java
│ │ │ ├── NullSpell.java
│ │ │ ├── OverrideTargetSpell.java
│ │ │ ├── PutCopyInHandSpell.java
│ │ │ ├── PutMinionOnBoardFromDeckSpell.java
│ │ │ ├── PutMinionOnBoardSpell.java
│ │ │ ├── PutRandomMinionOnBoardSpell.java
│ │ │ ├── PutRandomSecretIntoPlaySpell.java
│ │ │ ├── RandomAttackTargetSpell.java
│ │ │ ├── RandomSpellTargetSpell.java
│ │ │ ├── RandomlyCastSpell.java
│ │ │ ├── RecastSpell.java
│ │ │ ├── ReceiveCardAndDoSomethingSpell.java
│ │ │ ├── ReceiveCardSpell.java
│ │ │ ├── ReceiveRandomCardSpell.java
│ │ │ ├── RefreshHeroPowerSpell.java
│ │ │ ├── RemoveAttributeSpell.java
│ │ │ ├── RemoveCardSpell.java
│ │ │ ├── RenounceClassSpell.java
│ │ │ ├── ReplaceCardLocationSpell.java
│ │ │ ├── ResurrectFromBothSpell.java
│ │ │ ├── ResurrectSpell.java
│ │ │ ├── ReturnMinionToHandSpell.java
│ │ │ ├── RevertableSpell.java
│ │ │ ├── ReviveMinionSpell.java
│ │ │ ├── SetAttackSpell.java
│ │ │ ├── SetHeroHpSpell.java
│ │ │ ├── SetHpSpell.java
│ │ │ ├── ShuffleMinionToDeckSpell.java
│ │ │ ├── ShuffleToDeckSpell.java
│ │ │ ├── SilenceSpell.java
│ │ │ ├── Spell.java
│ │ │ ├── SpellUtils.java
│ │ │ ├── StealRandomSecretSpell.java
│ │ │ ├── SummonCopySpell.java
│ │ │ ├── SummonNewAttackTargetSpell.java
│ │ │ ├── SummonOneOneCopySpell.java
│ │ │ ├── SummonRandomMinionFilteredSpell.java
│ │ │ ├── SummonRandomNotOnBoardSpell.java
│ │ │ ├── SummonRandomSpell.java
│ │ │ ├── SummonSpell.java
│ │ │ ├── SwapAttackAndHpSpell.java
│ │ │ ├── SwapAttackSpell.java
│ │ │ ├── SwapHpSpell.java
│ │ │ ├── SwipeSpell.java
│ │ │ ├── TargetPlayer.java
│ │ │ ├── TemporaryAttackSpell.java
│ │ │ ├── TransformCardSpell.java
│ │ │ ├── TransformMinionSpell.java
│ │ │ ├── TransformToRandomMinionSpell.java
│ │ │ ├── TriggerDeathrattleSpell.java
│ │ │ ├── aura/
│ │ │ │ ├── AttributeAura.java
│ │ │ │ ├── Aura.java
│ │ │ │ ├── BuffAura.java
│ │ │ │ └── EnrageAura.java
│ │ │ ├── custom/
│ │ │ │ ├── AlarmOBotSpell.java
│ │ │ │ ├── BetrayalSpell.java
│ │ │ │ ├── FacelessSpell.java
│ │ │ │ ├── HeraldVolajzSpell.java
│ │ │ │ ├── HolyWrathSpell.java
│ │ │ │ ├── KelThuzadSpell.java
│ │ │ │ ├── MadamGoyaSpell.java
│ │ │ │ ├── MergeSpell.java
│ │ │ │ ├── MoatLurkerSpell.java
│ │ │ │ ├── PoisonSeedsSpell.java
│ │ │ │ ├── PutMiniCopyInHandSpell.java
│ │ │ │ ├── ShadowMadnessSpell.java
│ │ │ │ └── ShifterZerusSpell.java
│ │ │ ├── desc/
│ │ │ │ ├── BattlecryDesc.java
│ │ │ │ ├── ISpellConditionChecker.java
│ │ │ │ ├── SpellArg.java
│ │ │ │ ├── SpellDesc.java
│ │ │ │ ├── SpellFactory.java
│ │ │ │ ├── aura/
│ │ │ │ │ ├── AuraArg.java
│ │ │ │ │ └── AuraDesc.java
│ │ │ │ ├── condition/
│ │ │ │ │ ├── AndCondition.java
│ │ │ │ │ ├── AttributeCondition.java
│ │ │ │ │ ├── CardCountCondition.java
│ │ │ │ │ ├── CardPropertyCondition.java
│ │ │ │ │ ├── ComboCondition.java
│ │ │ │ │ ├── ComparisonCondition.java
│ │ │ │ │ ├── Condition.java
│ │ │ │ │ ├── ConditionArg.java
│ │ │ │ │ ├── ConditionDesc.java
│ │ │ │ │ ├── ControlsSecretCondition.java
│ │ │ │ │ ├── DeckContainsCondition.java
│ │ │ │ │ ├── GraveyardContainsCondition.java
│ │ │ │ │ ├── GraveyardCountCondition.java
│ │ │ │ │ ├── HasAttackedCondition.java
│ │ │ │ │ ├── HasEntitiesOnBoardCondition.java
│ │ │ │ │ ├── HasEntityCondition.java
│ │ │ │ │ ├── HasHeroPowerCondition.java
│ │ │ │ │ ├── HasWeaponCondition.java
│ │ │ │ │ ├── HighlanderDeckCondition.java
│ │ │ │ │ ├── HoldsCardCondition.java
│ │ │ │ │ ├── IsDamagedCondition.java
│ │ │ │ │ ├── IsDeadCondition.java
│ │ │ │ │ ├── ManaCostCondition.java
│ │ │ │ │ ├── ManaMaxedCondition.java
│ │ │ │ │ ├── MinionCountCondition.java
│ │ │ │ │ ├── MinionOnBoardCondition.java
│ │ │ │ │ ├── OrCondition.java
│ │ │ │ │ ├── OwnedByPlayerCondition.java
│ │ │ │ │ ├── RaceCondition.java
│ │ │ │ │ └── RandomCondition.java
│ │ │ │ ├── filter/
│ │ │ │ │ ├── AndFilter.java
│ │ │ │ │ ├── AttributeFilter.java
│ │ │ │ │ ├── CardFilter.java
│ │ │ │ │ ├── DamagedFilter.java
│ │ │ │ │ ├── EntityFilter.java
│ │ │ │ │ ├── FilterArg.java
│ │ │ │ │ ├── FilterDesc.java
│ │ │ │ │ ├── HighestAttributeFilter.java
│ │ │ │ │ ├── InDeckFilter.java
│ │ │ │ │ ├── InHandFilter.java
│ │ │ │ │ ├── Operation.java
│ │ │ │ │ ├── OrFilter.java
│ │ │ │ │ ├── RaceFilter.java
│ │ │ │ │ └── SpecificCardFilter.java
│ │ │ │ ├── manamodifier/
│ │ │ │ │ ├── CardCostModifierArg.java
│ │ │ │ │ └── CardCostModifierDesc.java
│ │ │ │ ├── source/
│ │ │ │ │ ├── CardSource.java
│ │ │ │ │ ├── DeckSource.java
│ │ │ │ │ ├── DefaultSource.java
│ │ │ │ │ ├── HandSource.java
│ │ │ │ │ ├── SourceArg.java
│ │ │ │ │ └── SourceDesc.java
│ │ │ │ ├── trigger/
│ │ │ │ │ ├── EventTriggerArg.java
│ │ │ │ │ ├── EventTriggerDesc.java
│ │ │ │ │ ├── EventTriggerDeserializer.java
│ │ │ │ │ └── TriggerDesc.java
│ │ │ │ └── valueprovider/
│ │ │ │ ├── AlgebraicOperation.java
│ │ │ │ ├── AlgebraicValueProvider.java
│ │ │ │ ├── AttributeCounter.java
│ │ │ │ ├── AttributeValueProvider.java
│ │ │ │ ├── CardCounter.java
│ │ │ │ ├── CardsPlayedValueProvider.java
│ │ │ │ ├── ConditionalValueProvider.java
│ │ │ │ ├── DeadMinionsThisTurn.java
│ │ │ │ ├── EntityCounter.java
│ │ │ │ ├── HighestAttributeValueProvider.java
│ │ │ │ ├── MinionSummonValueProvider.java
│ │ │ │ ├── PlayerAttributeValueProvider.java
│ │ │ │ ├── RandomValueProvider.java
│ │ │ │ ├── ValueProvider.java
│ │ │ │ ├── ValueProviderArg.java
│ │ │ │ └── ValueProviderDesc.java
│ │ │ └── trigger/
│ │ │ ├── AfterMinionPlayedTrigger.java
│ │ │ ├── AfterMinionSummonedTrigger.java
│ │ │ ├── AfterPhysicalAttackTrigger.java
│ │ │ ├── AfterSpellCastedTrigger.java
│ │ │ ├── ArmorGainedTrigger.java
│ │ │ ├── BeforeMinionPlayedTrigger.java
│ │ │ ├── BeforeMinionSummonedTrigger.java
│ │ │ ├── BoardChangedTrigger.java
│ │ │ ├── CardDrawnTrigger.java
│ │ │ ├── CardPlayedTrigger.java
│ │ │ ├── CardReceivedTrigger.java
│ │ │ ├── DamageCausedTrigger.java
│ │ │ ├── DamageReceivedTrigger.java
│ │ │ ├── DiscardTrigger.java
│ │ │ ├── EnrageChangedTrigger.java
│ │ │ ├── FatalDamageTrigger.java
│ │ │ ├── GameEventTrigger.java
│ │ │ ├── GameStartTrigger.java
│ │ │ ├── GameStateChangedTrigger.java
│ │ │ ├── HealingTrigger.java
│ │ │ ├── IGameEventListener.java
│ │ │ ├── InspireTrigger.java
│ │ │ ├── MinionDeathTrigger.java
│ │ │ ├── MinionPlayedTrigger.java
│ │ │ ├── MinionSummonedTrigger.java
│ │ │ ├── OverloadTrigger.java
│ │ │ ├── PhysicalAttackTrigger.java
│ │ │ ├── PreDamageTrigger.java
│ │ │ ├── QuestPlayedTrigger.java
│ │ │ ├── QuestSuccessTrigger.java
│ │ │ ├── SecretPlayedTrigger.java
│ │ │ ├── SecretRevealedTrigger.java
│ │ │ ├── SilenceTrigger.java
│ │ │ ├── SpellCastedTrigger.java
│ │ │ ├── SpellTrigger.java
│ │ │ ├── TargetAcquisitionTrigger.java
│ │ │ ├── TriggerManager.java
│ │ │ ├── TurnEndTrigger.java
│ │ │ ├── TurnStartTrigger.java
│ │ │ ├── WeaponDestroyedTrigger.java
│ │ │ ├── WeaponEquippedTrigger.java
│ │ │ └── types/
│ │ │ ├── Quest.java
│ │ │ └── Secret.java
│ │ ├── statistics/
│ │ │ ├── GameStatistics.java
│ │ │ └── Statistic.java
│ │ ├── targeting/
│ │ │ ├── CardLocation.java
│ │ │ ├── CardReference.java
│ │ │ ├── EntityReference.java
│ │ │ ├── IdFactory.java
│ │ │ ├── TargetSelection.java
│ │ │ └── TargetType.java
│ │ └── utils/
│ │ ├── GameTagUtils.java
│ │ └── TagValueType.java
│ └── test/
│ └── java/
│ └── net/
│ └── demilich/
│ └── metastone/
│ └── tests/
│ ├── AdvancedMechanicTests.java
│ ├── AuraTests.java
│ ├── BasicTests.java
│ ├── BlackrockMountainTests.java
│ ├── CardInteractionTests.java
│ ├── CloningTest.java
│ ├── DebugContext.java
│ ├── HeroPowerTest.java
│ ├── ManaTests.java
│ ├── MassTest.java
│ ├── PoisonSeedsTests.java
│ ├── SecretTest.java
│ ├── SpecialCardTests.java
│ ├── TargetingTests.java
│ ├── TechnicalTests.java
│ ├── TestAction.java
│ ├── TestBase.java
│ ├── TestMinionCard.java
│ ├── TestSecretCard.java
│ ├── TestSpellCard.java
│ ├── TheOldGodsTests.java
│ ├── WeaponTests.java
│ └── allcards/
│ ├── ClassicMageCards.java
│ └── ClassicNeutralCards.java
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── shared/
├── build.gradle
├── lib/
│ └── nitty-gritty-mvc.jar
└── src/
└── main/
└── java/
└── net/
└── demilich/
└── metastone/
├── GameNotification.java
├── NotificationProxy.java
├── game/
│ └── behaviour/
│ └── threat/
│ ├── FeatureVector.java
│ └── WeightedFeature.java
├── trainingmode/
│ ├── ITrainingDataListener.java
│ ├── RequestTrainingDataNotification.java
│ └── TrainingData.java
└── utils/
├── ICallback.java
├── IDisposable.java
├── MathUtils.java
├── MetastoneProperties.java
├── ResourceInputStream.java
├── ResourceLoader.java
├── Tuple.java
├── UserHomeMetastone.java
└── VersionInfo.java
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
bin
.settings
.classpath
.project
.gradle
.idea
test-output
report.log
/~$card_checklist.xlsx
build/
classes/
*.iml
cards/src/main/resources/metastone.properties
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
================================================
FILE: README.md
================================================
# MetaStone #
### What is it? ###
MetaStone is a simulator for the online collectible card game (CCG) Hearthstone® by Activison Blizzard written in Java. It strives to be a useful tool for card value analyzation, deck building and performance evaluation. There is also support for custom cards, allowing users to implement their own card inventions and testing them within the simulator engine. MetaStone tries to re-implement all game mechanics and rules from the original game as accurately as possible.
### What is it not? ###
This is no Hearthstone replacement or clone. Please do not request better graphical effects, sounds or anything which makes it feel more like Hearthstone. There won't be any mode to battle against other human players. This is a tool meant for advanced players; if you just want to play Hearthstone, please play the real game.
### How do I run it on Linux? ###
* Go to [Releases](https://github.com/demilich1/metastone/releases) and download the latest release (`metastone-X_Y_Z_jar.zip`).
* Extract the contents of the .zip file.
* Open the Terminal (`Ctrl+Alt+T` on Ubuntu) and access `../MetaStone-X.Y.Z/bin`.
* Execute this command: `./MetaStone`.
* Executing `sudo ./MetaStone` will execute the file as Root ("Super User"), this is not necessary though.
* You might need to make the file executable (On Ubuntu: Right Click the File -> Properties -> Permissions -> Allow executing file as program).
### Can I contribute? ###
Sure! There is still a lot to do and anybody willing to contribute is welcome
### What needs to be done? ###
- UI improvements in general are welcome
- We always need more unit tests! If you don't know what to test, take a look at http://hearthstone.gamepedia.com/Advanced_rulebook and just pick an example of card interaction from that wiki page
- Code refactorings to make the code simpler and/or faster
- There is a bug in the code and you know how to fix it? Great!
- Better AI: at the moment the most advanced AI is 'Game State Value', however it is very subpar compared to human players. A more sophisticated AI would be a huge boon
- Also consider having a look at the open issues
- Anything else you would like to improve
### How do I compile the code on my machine? ###
* NOTE **JDK 1.8 is required!**
* Clone the repo. See [https://help.github.com/articles/cloning-a-repository/](https://help.github.com/articles/cloning-a-repository/) for help.
* Open a terminal / command prompt and nagivate to your to your git repo location
* Run the application from the command line:
* Linux/Mac OSX `./gradlew run`
* Windows `gradlew.bat run`
* Note: this will download all dependecies, compile and assemble all modules and then run the app.
* Download dependecies and compile:
* Linux/Mac OSX `./gradlew compileJava`
* Windows `gradlew.bat compileJava`
* Note: this will download all dependecies and compile all modules. Usefull when developing.
* Get a list of all gradle tasks:
* Linux/Mac OSX `./gradlew tasks --all`
* Windows `gradlew.bat tasks --all`
#### Building with an IDE
* If you want to build from Eclipse, create the Eclipse project files:
* Linux/Mac OSX `./gradlew eclipse`
* Windows `gradlew.bat eclipse`
* _The above gradle task will automatically generate the `BuildConfig.java` file._
* Open Eclipse and choose `File > Import > General > Existing projects into workspace`
* Select the `Search for nested project` checkbox on the `Import Projects` screen.
* Change `Eclipse > Window > Preferences > Java > Compiler > Compiler Complience Level` to 1.8
* Change `Eclipse > Window > Preferences > Java > Compiler > Building > Circular dependencies` from `Error` to `Warning`. There is a [known bug](https://issues.gradle.org/browse/GRADLE-2200) with importing multi-module gradle projects into Eclipse. The IDE of choice for working with gradle projects is [IntelliJ IDEA](https://www.jetbrains.com/idea/).
* If you want to build from IntelliJ IDEA, create the IntelliJ project files:
* Linux/Mac OSX `./gradlew idea`
* Windows `gradlew.bat idea`
* _The above gradle task will automatically generate the `BuildConfig.java` file._
* Open IntelliJ and select `File > Open` then navigate to the project root dir.
* ***Optionally*** (advanced option), you can choose to import the project into your respective IDE from the `build.gradle` files. When doing so, you **must** manually generate the `BuildConfig.java` file. Otherwise your IDE will complain about unresolved references to `BuildConfig.java`.
* Linux/Mac OSX `./gradlew compileBuildConfig`
* Windows `gradlew.bat compileBuildConfig`
### Project structure
* MetaStone is made up of a handfull of source modules. Here's what the top level structure looks like:
```
metastone
├── app // Application UI code and resources. Depends on 'game' and 'cards' modules.
├── game // Game source code. Depends on 'shared' module.
├── shared // Shared code between 'app' and 'game' modules.
└── cards // Cards, decks and deckFormat data files.
```
* Each module can be built separately. Their respective dependencies will get compiled and pulled in at build time. For example:
* To produce a `cards.jar` file which contains all the cards, decks and deckFormat data files:
* Linux/Mac OSX `./gradlew cards:assemble`
* Windows `gradlew.bat cards:assemble`
* To build the game module and produce a `game.jar` file:
* Linux/Mac OSX `./gradlew game:assemble`
* Windows `gradlew.bat game:assemble`
* To produce a standalone distributable app binary:
* Linux/Mac OSX `./gradlew app:assemble`
* Windows `gradlew.bat app:assemble`
### How do I build my own cards? ###
**This feature is in very early stages and there is no official support yet.** There is no documentation at all. If you really want to start right now, here's how you can start:
- You can build your own cards or modify existing cards without having to fork the project!
- Card files are located in the `metastone/cards` directory. **Use these as reference!**
* Linux/Mac OSX `~/metastone/cards`
* Windows `C:\Users\[username]\Documents\metastone\cards`
* You can override the default metastone home dir by setting an environment varialble `USER_HOME_METASTONE` and specifying a new path.
* You must launch the app at least once for card data files to be copied.
- Any `.json` files you place in your `metastone/cards` folder will be parsed and treated like built-in cards.
- To learn the cards format it is highly recommended that you copy an existing card, change the `filename` and the `id` attribute (**<-- important!**) and make small changes.
- Restart MetaStone for new cards to be detected.
- If you are building out official cards or fixing existing cards, you will need to fork the project then make your changes in your repo's `metastone/cards/src/main/resources/cards` dir. Then open a [Pull Request](https://help.github.com/articles/using-pull-requests/) into the project [master](https://github.com/demilich1/metastone/tree/master) branch with your changes.
- Make sure to validate that the cards you added are well formed and can be parsed! Run the following command:
- Linux/Mac OSX `./gradlew cards:test`
- Windows `gradlew.bat cards:test`
- **The card format is subject to change; cards you create now MAY NOT work in future versions**
- In the rare chance that your card files get messed up beyond repair, you can always force the app to overwrite your local card files with the versions distributed with the app in `cards.jar`.
* _Option 1_: Delete the `~/metastone` dir.
* You **WILL LOOSE** all your changes, including **ALL new files** you may have added. DANGEROUS! MAKE A BACKUP!!
* Linux/Mac OSX `rm -rf ~/metastone`
* Windows `rmdir /s C:\Users\[username]\Documents\metastone`
* Card data files will be copied in their prestine state after you restart the app.
* _Option 2_: Edit the `~/metastone/metastone.properties` file and update the `cards.copied` property.
* delete the `cards.copied` property and save the file
* New files you may have added will NOT be affected.
* All card files that are distributed with the app will be overritten after you restart the app.
### Running tests
* The easiest way to run tests is from the command line.
* Linux/Mac OSX `./gradlew game:test`
* Windows `gradlew.bat game:test`
* You can also run tests from your favorite IDE. For example:
* In IntelliJ right click on `src/test` folder in a given module and select `Run All Tests`
* You can also run individual tests using the `-Dtest.single=[TEST NAME]` command line option.
* From the command line
* Linux/Mac OSX `./gradlew game:test -Dtest.single=SecretTest`
* Windows `gradlew.bat game:test -Dtest.single=SecretTest`
* From your IDE
* Right click on the individual test file and select `Run Test`
* If you encounter test failures open the test report file `build/reports/tests/index.html` for details on the failures
* Look [**here**](/game/src/test/java/net/demilich/metastone/tests) for list of existing game tests.
================================================
FILE: app/build.fxbuild
================================================
================================================
FILE: app/build.gradle
================================================
apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'javafx-gradle-plugin'
buildscript {
dependencies {
classpath group: 'de.dynamicfiles.projects.gradle.plugins', name: 'javafx-gradle-plugin', version: '8.8.2'
}
repositories {
jcenter()
}
}
mainClassName = 'net.demilich.metastone.MetaStone'
jar {
manifest {
attributes 'Implementation-Title': rootProject.name.capitalize(),
'Implementation-Version': project.version
}
}
dependencies {
compile project(':game')
compile project(':cards')
compile files('lib/controlsfx-8.40.10-20151003.010657-492.jar')
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.4'
compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5'
compile 'org.jsoup:jsoup:1.10.2'
compile 'org.controlsfx:openjfx-dialogs:1.0.2'
testCompile group: 'org.testng', name: 'testng', version: '6.+'
}
jfx {
// minimal requirement for jfxJar-task
mainClass = 'JavaFXDemo'
// minimal requirement for jfxNative-task
vendor = ''
}
test {
// enable TestNG support (default is JUnit)
useTestNG()
testLogging {
events "standardError"
}
}
================================================
FILE: app/javafx.plugin
================================================
/*
* Bootstrap script for the Gradle JavaFX Plugin.
* (based on http://plugins.jasoft.fi/vaadin.plugin)
*
* The script will add the plugin to the build script
* dependencies and apply the plugin to the project. If you do not want
* this behavior you can copy and paste the below configuration into your
* own build script and define your own repository and version for the plugin.
*/
import org.gradle.api.GradleException;
buildscript {
repositories {
mavenLocal()
maven {
name = 'BinTray'
url = 'http://dl.bintray.com/content/shemnon/javafx-gradle/'
}
maven {
name = 'CloudBees Snapshot'
url = 'http://repository-javafx-gradle-plugin.forge.cloudbees.com/snapshot'
}
ivy {
url = 'http://repository-javafx-gradle-plugin.forge.cloudbees.com/snapshot'
}
mavenCentral()
}
dependencies {
try {
assert (jfxrtDir != null)
} catch (RuntimeException re) {
ext.jfxrtDir = "."
}
ext.searchFile = {Map places, List searchPaths, String searchID ->
File result = null;
places.each { k, v ->
if (result != null) return;
project.logger.debug("Looking for $searchID in $k")
def dir = v()
if (dir == null) {
project.logger.debug("$k not set")
} else {
project.logger.debug("$k is $dir")
searchPaths.each { s ->
if (result != null) return;
File f = new File(dir, s);
project.logger.debug("Trying $f.path")
if (f.exists() && f.file) {
project.logger.debug("found $searchID as $result")
result = f;
}
}
}
}
if (!result?.file) {
throw new GradleException("Could not find $searchID, please set one of ${places.keySet()}");
} else {
project.logger.info("$searchID: ${result}")
return result
}
}
ext.findJFXJar = {
return searchFile([
'jfxrtDir in Gradle Properties': {jfxrtDir},
'JFXRT_HOME in System Environment': {System.env['JFXRT_HOME']},
'JAVA_HOME in System Environment': {System.env['JAVA_HOME']},
'java.home in JVM properties': {System.properties['java.home']}
],
['jfxrt.jar', 'lib/jfxrt.jar', 'lib/ext/jfxrt.jar', 'jre/lib/jfxrt.jar', 'jre/lib/ext/jfxrt.jar'],
'JavaFX Runtime Jar')
}
ext.findAntJavaFXJar = {
return searchFile([
'jfxrtDir in Gradle Properties': {jfxrtDir},
'JFXRT_HOME in System Environment': {System.env['JFXRT_HOME']},
'JAVA_HOME in System Environment': {System.env['JAVA_HOME']},
'java.home in JVM properties': {System.properties['java.home']}
],
['ant-javafx.jar', 'lib/ant-javafx.jar', '../lib/ant-javafx.jar'],
'JavaFX Packager Tools')
}
classpath 'org.bitbucket.shemnon.javafxplugin:gradle-javafx-plugin:8.1.2-SNAPSHOT'
classpath project.files(findAntJavaFXJar())
classpath project.files(findJFXJar())
}
}
if (!project.plugins.findPlugin(org.bitbucket.shemnon.javafxplugin.JavaFXPlugin)) {
project.apply(plugin: org.bitbucket.shemnon.javafxplugin.JavaFXPlugin)
}
================================================
FILE: app/manifest.json
================================================
{
"version" : "1.2.0",
"whatsNew" : [
"- added all 'One Night in Karazhan' cards"
]
}
================================================
FILE: app/src/deploy/package/windows/Metastone.iss
================================================
;This file will be executed next to the application bundle image
;I.e. current directory will contain folder Metastone with application files
[Setup]
AppId={{fxApplication}}
AppName=Metastone
AppVersion=1.2.0
AppVerName=Metastone
AppPublisher=demilich
AppComments=MetaStone
AppCopyright=Copyright (C) 2016
;AppPublisherURL=http://java.com/
;AppSupportURL=http://java.com/
;AppUpdatesURL=http://java.com/
DefaultDirName={localappdata}\Metastone
DisableStartupPrompt=Yes
DisableDirPage=No
DisableProgramGroupPage=Yes
DisableReadyPage=Yes
DisableFinishedPage=No
DisableWelcomePage=No
DefaultGroupName=MetaStone
;Optional License
LicenseFile=
;WinXP or above
MinVersion=0,5.1
OutputBaseFilename=Metastone_Installer
Compression=lzma
SolidCompression=yes
PrivilegesRequired=lowest
SetupIconFile=Metastone\Metastone.ico
UninstallDisplayIcon={app}\Metastone.ico
UninstallDisplayName=Metastone
WizardImageStretch=No
WizardSmallImageFile=Metastone-setup-icon.bmp
ArchitecturesInstallIn64BitMode=
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "Metastone\Metastone.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "Metastone\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\Metastone"; Filename: "{app}\Metastone.exe"; IconFilename: "{app}\Metastone.ico"; Check: returnTrue()
Name: "{commondesktop}\Metastone"; Filename: "{app}\Metastone.exe"; IconFilename: "{app}\Metastone.ico"; Check: returnFalse()
[Run]
Filename: "{app}\Metastone.exe"; Description: "{cm:LaunchProgram,Metastone}"; Flags: nowait postinstall skipifsilent; Check: returnTrue()
Filename: "{app}\Metastone.exe"; Parameters: "-install -svcName ""Metastone"" -svcDesc ""Metastone"" -mainExe ""Metastone.exe"" "; Check: returnFalse()
[UninstallRun]
Filename: "{app}\Metastone.exe "; Parameters: "-uninstall -svcName Metastone -stopOnUninstall"; Check: returnFalse()
[Code]
function returnTrue(): Boolean;
begin
Result := True;
end;
function returnFalse(): Boolean;
begin
Result := False;
end;
function InitializeSetup(): Boolean;
begin
// Possible future improvements:
// if version less or same => just launch app
// if upgrade => check if same app is running and wait for it to exit
// Add pack200/unpack200 support?
Result := True;
end;
================================================
FILE: app/src/main/java/net/demilich/metastone/ApplicationFacade.java
================================================
package net.demilich.metastone;
import net.demilich.nittygrittymvc.Facade;
import net.demilich.nittygrittymvc.interfaces.IFacade;
import net.demilich.metastone.gui.autoupdate.CheckForUpdateCommand;
import net.demilich.metastone.gui.battleofdecks.StartBattleOfDecksCommand;
import net.demilich.metastone.gui.deckbuilder.AddCardToDeckCommand;
import net.demilich.metastone.gui.deckbuilder.ChangeDeckNameCommand;
import net.demilich.metastone.gui.deckbuilder.DeleteDeckCommand;
import net.demilich.metastone.gui.deckbuilder.FillDeckWithRandomCardsCommand;
import net.demilich.metastone.gui.deckbuilder.FilterCardsCommand;
import net.demilich.metastone.gui.deckbuilder.ImportDeckCommand;
import net.demilich.metastone.gui.deckbuilder.LoadDeckFormatsCommand;
import net.demilich.metastone.gui.deckbuilder.LoadDecksCommand;
import net.demilich.metastone.gui.deckbuilder.RemoveCardFromDeckCommand;
import net.demilich.metastone.gui.deckbuilder.SaveDeckCommand;
import net.demilich.metastone.gui.deckbuilder.SetActiveDeckCommand;
import net.demilich.metastone.gui.deckbuilder.metadeck.AddDeckToMetaDeckCommand;
import net.demilich.metastone.gui.deckbuilder.metadeck.RemoveDeckFromMetaDeckCommand;
import net.demilich.metastone.gui.playmode.StartGameCommand;
import net.demilich.metastone.gui.playmode.animation.AnimationCompletedCommand;
import net.demilich.metastone.gui.playmode.animation.AnimationLockCommand;
import net.demilich.metastone.gui.playmode.animation.AnimationStartedCommand;
import net.demilich.metastone.gui.playmode.config.RequestDeckFormatsCommand;
import net.demilich.metastone.gui.playmode.config.RequestDecksCommand;
import net.demilich.metastone.gui.sandboxmode.commands.CreateNewSandboxCommand;
import net.demilich.metastone.gui.sandboxmode.commands.ModifyPlayerDeckCommand;
import net.demilich.metastone.gui.sandboxmode.commands.ModifyPlayerHandCommand;
import net.demilich.metastone.gui.sandboxmode.commands.PerformActionCommand;
import net.demilich.metastone.gui.sandboxmode.commands.SelectPlayerCommand;
import net.demilich.metastone.gui.sandboxmode.commands.SpawnMinionCommand;
import net.demilich.metastone.gui.sandboxmode.commands.StartPlaySandboxCommand;
import net.demilich.metastone.gui.sandboxmode.commands.StopPlaySandboxCommand;
import net.demilich.metastone.gui.simulationmode.SimulateGamesCommand;
import net.demilich.metastone.gui.trainingmode.PerformTrainingCommand;
import net.demilich.metastone.gui.trainingmode.RequestTrainingDataCommand;
import net.demilich.metastone.gui.trainingmode.SaveTrainingDataCommand;
public class ApplicationFacade extends Facade {
@SuppressWarnings("unchecked")
public static IFacade getInstance() {
if (instance == null) {
instance = new ApplicationFacade();
}
return instance;
}
public ApplicationFacade() {
NotificationProxy.init(this);
registerCommand(GameNotification.APPLICATION_STARTUP, new ApplicationStartupCommand());
registerCommand(GameNotification.START_GAME, new StartGameCommand());
registerCommand(GameNotification.PLAY_GAME, new PlayGameCommand());
registerCommand(GameNotification.SIMULATE_GAMES, new SimulateGamesCommand());
registerCommand(GameNotification.START_TRAINING, new PerformTrainingCommand());
registerCommand(GameNotification.COMMIT_BATTLE_OF_DECKS_CONFIG, new StartBattleOfDecksCommand());
registerCommand(GameNotification.CHECK_FOR_UPDATE, new CheckForUpdateCommand());
registerCommand(GameNotification.SET_ACTIVE_DECK, new SetActiveDeckCommand());
registerCommand(GameNotification.ADD_CARD_TO_DECK, new AddCardToDeckCommand());
registerCommand(GameNotification.REMOVE_CARD_FROM_DECK, new RemoveCardFromDeckCommand());
registerCommand(GameNotification.SAVE_ACTIVE_DECK, new SaveDeckCommand());
registerCommand(GameNotification.LOAD_DECKS, new LoadDecksCommand());
registerCommand(GameNotification.LOAD_DECK_FORMATS, new LoadDeckFormatsCommand());
registerCommand(GameNotification.FILTER_CARDS, new FilterCardsCommand());
registerCommand(GameNotification.FILL_DECK_WITH_RANDOM_CARDS, new FillDeckWithRandomCardsCommand());
registerCommand(GameNotification.IMPORT_DECK_FROM_URL, new ImportDeckCommand());
registerCommand(GameNotification.CHANGE_DECK_NAME, new ChangeDeckNameCommand());
registerCommand(GameNotification.ADD_DECK_TO_META_DECK, new AddDeckToMetaDeckCommand());
registerCommand(GameNotification.REMOVE_DECK_FROM_META_DECK, new RemoveDeckFromMetaDeckCommand());
registerCommand(GameNotification.DELETE_DECK, new DeleteDeckCommand());
registerCommand(GameNotification.REQUEST_DECKS, new RequestDecksCommand());
registerCommand(GameNotification.REQUEST_DECK_FORMATS, new RequestDeckFormatsCommand());
registerCommand(GameNotification.CREATE_NEW_SANDBOX, new CreateNewSandboxCommand());
registerCommand(GameNotification.MODIFY_PLAYER_DECK, new ModifyPlayerDeckCommand());
registerCommand(GameNotification.MODIFY_PLAYER_HAND, new ModifyPlayerHandCommand());
registerCommand(GameNotification.SELECT_PLAYER, new SelectPlayerCommand());
registerCommand(GameNotification.SPAWN_MINION, new SpawnMinionCommand());
registerCommand(GameNotification.PERFORM_ACTION, new PerformActionCommand());
registerCommand(GameNotification.START_PLAY_SANDBOX, new StartPlaySandboxCommand());
registerCommand(GameNotification.STOP_PLAY_SANDBOX, new StopPlaySandboxCommand());
registerCommand(GameNotification.GAME_STATE_UPDATE, new AnimationLockCommand());
registerCommand(GameNotification.ANIMATION_STARTED, new AnimationStartedCommand());
registerCommand(GameNotification.ANIMATION_COMPLETED, new AnimationCompletedCommand());
registerCommand(GameNotification.SAVE_TRAINING_DATA, new SaveTrainingDataCommand());
registerCommand(GameNotification.REQUEST_TRAINING_DATA, new RequestTrainingDataCommand());
}
public void startUp() {
sendNotification(GameNotification.APPLICATION_STARTUP);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/ApplicationStartupCommand.java
================================================
package net.demilich.metastone;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.gui.cards.CardProxy;
import net.demilich.metastone.gui.autoupdate.AutoUpdateMediator;
import net.demilich.metastone.gui.deckbuilder.DeckFormatProxy;
import net.demilich.metastone.gui.deckbuilder.DeckProxy;
import net.demilich.metastone.gui.dialog.DialogMediator;
import net.demilich.metastone.gui.main.ApplicationMediator;
import net.demilich.metastone.gui.playmode.animation.AnimationProxy;
import net.demilich.metastone.gui.sandboxmode.SandboxProxy;
import net.demilich.metastone.gui.trainingmode.TrainingProxy;
public class ApplicationStartupCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
getFacade().registerMediator(new DialogMediator());
getFacade().registerProxy(new CardProxy());
getFacade().registerProxy(new DeckProxy());
getFacade().registerProxy(new DeckFormatProxy());
getFacade().registerProxy(new TrainingProxy());
getFacade().registerProxy(new SandboxProxy());
getFacade().registerProxy(new AnimationProxy());
getFacade().registerMediator(new ApplicationMediator());
getFacade().registerMediator(new AutoUpdateMediator());
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/DevCardTools.java
================================================
package net.demilich.metastone;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.apache.commons.io.FileUtils;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.cards.CardCatalogue;
/**
* TODO: not sure how this is used.
* Paths in this file are no longer valid since cards, decks and deckformat are loaded from jar resources
*/
public class DevCardTools {
public static void assignUniqueIdToEachCard() {
final String path = "./src/" + Card.class.getPackage().getName().replace(".", "/") + "/concrete/";
final String idExpression = "public int getTypeId()";
File folder = new File(path);
int uniqueId = 1;
HashSet assignedIds = new HashSet<>();
List filesWithoutId = new ArrayList<>();
for (File file : FileUtils.listFiles(folder, new String[] { "java" }, true)) {
try {
// System.out.println("Processing " + file.getName() + "...");
List lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
int lineIndex = containsExpression(lines, idExpression);
if (lineIndex != -1) {
// System.out.println("Skipping " + file.getName() +
// " because it already has an id assigned");
int id = extractId(lines.get(lineIndex + 1));
assignedIds.add(id);
continue;
} else {
filesWithoutId.add(file);
}
} catch (IOException e) {
System.err.println("Error while parsing file: " + file.getName());
e.printStackTrace();
}
}
while (assignedIds.contains(uniqueId)) {
uniqueId++;
}
for (File file : filesWithoutId) {
try {
List lines;
lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
for (int i = lines.size() - 1; i > 0; i--) {
String line = lines.get(i);
if (line.contains("}")) {
lines.add(i, "\t}");
lines.add(i, "\t\treturn " + uniqueId + ";");
lines.add(i, "\tpublic int getTypeId() {");
lines.add(i, "\t@Override");
lines.add(i, "\n");
System.out.println("Assigning id " + uniqueId + " to " + file.getName());
uniqueId++;
break;
}
}
Files.write(file.toPath(), lines, StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void cardListFromImages(String path) throws IOException {
File folder = new File(path);
PrintWriter out = new PrintWriter(new FileWriter("cards_all"));
for (File file : folder.listFiles()) {
if (file.isFile()) {
out.println(file.getName().replace(".png", ""));
}
}
out.close();
}
private static String changeFileNameToClassName(String name) {
if (name == null) {
throw new IllegalArgumentException("File Name == null");
}
String className = name.replace(".java", "");
className = className.replace('/', '.');
className = className.replace('\\', '.');
className = className.replace("..src.", "");
return className;
}
public static void compareClassesWithCardList(String path) throws IOException {
File folder = new File(path);
BufferedReader reader = new BufferedReader(new FileReader("cards_all"));
List allCards = new ArrayList();
String line;
while ((line = reader.readLine()) != null) {
allCards.add(toCanonName(line));
}
reader.close();
List allClasses = new ArrayList();
for (File file : FileUtils.listFiles(folder, new String[] { "java" }, true)) {
String canonName = toCanonName(file.getName());
allClasses.add(canonName);
}
int missing = 0;
for (String card : allCards) {
if (allClasses.contains(card)) {
// System.out.println("Card found: " + card);
} else {
missing++;
System.out.println("Card missing: " + card);
}
}
System.out.println("There are " + missing + " cards missing");
}
private static int containsExpression(List lines, String expression) {
int i = 0;
for (String line : lines) {
if (line.contains(expression)) {
return i;
}
i++;
}
return -1;
}
private static int extractId(String line) {
String result = "";
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (Character.isDigit(c)) {
result += c;
}
}
return Integer.parseInt(result);
}
public static void formatJsons() {
File folder = new File("./cards/");
Collection files = FileUtils.listFiles(folder, new String[] { "json" }, true);
int i = 0;
for (File file : files) {
try {
System.out.println("Processing " + file.getName() + " (" + ++i + " of " + files.size() + " files)");
prettyPrintFile(file);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private static List getImplementedCardsAsLines() {
final String expression = "cards.add(new %s());";
final String path = "./src/" + Card.class.getPackage().getName().replace(".", "/") + "/concrete/";
List lines = new ArrayList();
File folder = new File(path);
for (File file : FileUtils.listFiles(folder, new String[] { "java" }, true)) {
String cardFileName = file.getPath();
String cardClassName = changeFileNameToClassName(cardFileName);
// System.out.println(changeFileNameToClassName(cardName));
lines.add(String.format(expression, cardClassName));
}
return lines;
}
private static void prettyPrintFile(File file) throws IOException {
Path path = Paths.get(file.getPath());
String content = new String(Files.readAllBytes(path));
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine scriptEngine = manager.getEngineByName("JavaScript");
scriptEngine.put("jsonString", content);
try {
scriptEngine.eval("result = JSON.stringify(JSON.parse(jsonString), null, \"\t\")");
} catch (ScriptException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String prettyPrintedJson = (String) scriptEngine.get("result");
Files.write(path, prettyPrintedJson.getBytes());
}
private static String toCanonName(String name) {
return name.toLowerCase().replace(".java", "").replace(".png", "").replace("_", "").replace("-", "");
}
public static void updateCardCatalogue() {
final String cataloguePathStr = "./src/" + CardCatalogue.class.getPackage().getName().replace(".", "/") + "/CardCatalogue.java";
Path cataloguePath = Paths.get(cataloguePathStr);
List lines = new ArrayList<>();
try (BufferedReader reader = Files.newBufferedReader(cataloguePath)) {
String line = null;
List implementedCards = getImplementedCardsAsLines();
boolean insideRelevantCodeBlock = false;
while ((line = reader.readLine()) != null) {
if (line.contains("static {")) {
lines.add(line);
insideRelevantCodeBlock = true;
lines.addAll(implementedCards);
} else if (line.contains("}")) {
insideRelevantCodeBlock = false;
}
if (!insideRelevantCodeBlock) {
lines.add(line);
}
}
} catch (IOException x) {
System.err.format("IOException: %s%n", x);
}
try {
Files.write(cataloguePath, lines);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("CardCatalogue has been successfully updated");
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/MetaStone.java
================================================
package net.demilich.metastone;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import net.demilich.metastone.gui.IconFactory;
import net.demilich.metastone.utils.UserHomeMetastone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class MetaStone extends Application {
private static Logger logger = LoggerFactory.getLogger(MetaStone.class);
public static void main(String[] args) {
//DevCardTools.formatJsons();
try {
// ensure that the user home metastone dir exists
Files.createDirectories(Paths.get(UserHomeMetastone.getPath()));
} catch (IOException e) {
logger.error("Trouble creating " + Paths.get(UserHomeMetastone.getPath()));
e.printStackTrace();
}
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("MetaStone");
primaryStage.initStyle(StageStyle.UNIFIED);
primaryStage.setResizable(false);
primaryStage.getIcons().add(new Image(IconFactory.getImageUrl("ui/app_icon.png")));
ApplicationFacade facade = (ApplicationFacade) ApplicationFacade.getInstance();
facade.startUp();
StackPane root = new StackPane();
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("/css/main.css").toExternalForm());
primaryStage.setScene(scene);
// implementing potential visual fix for JavaFX
// setting the visual opacity to zero, and then
// once the stage is shown, setting the opacity to one.
// this fixes an issue where some users would only see a blank
// screen on application startup
primaryStage.setOpacity(0.0);
facade.sendNotification(GameNotification.CANVAS_CREATED, root);
facade.sendNotification(GameNotification.MAIN_MENU);
primaryStage.show();
// setting opacity to one for JavaFX hotfix
primaryStage.setOpacity(1.0);
facade.sendNotification(GameNotification.CHECK_FOR_UPDATE);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/PlayGameCommand.java
================================================
package net.demilich.metastone;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.game.GameContext;
public class PlayGameCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
GameContext context = (GameContext) notification.getBody();
context.play();
getFacade().sendNotification(GameNotification.GAME_OVER, context);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/DigitFactory.java
================================================
package net.demilich.metastone.gui;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import javax.imageio.ImageIO;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.effect.Blend;
import javafx.scene.effect.BlendMode;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.effect.ColorInput;
import javafx.scene.effect.Effect;
import javafx.scene.effect.ImageInput;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class DigitFactory {
private final static HashMap digits = new HashMap<>();
static {
digits.put('-', new Image(IconFactory.RESOURCE_PATH + "/img/common/digits/-.png"));
for (int i = 0; i < 10; i++) {
char digitToChar = Character.forDigit(i, 10);
digits.put(digitToChar, new Image(IconFactory.RESOURCE_PATH + "/img/common/digits/" + digitToChar + ".png"));
}
}
private static void applyFontColor(ImageView image, Color color) {
ColorAdjust monochrome = new ColorAdjust();
monochrome.setSaturation(-1.0);
Effect colorInput = new ColorInput(0, 0, image.getImage().getWidth(), image.getImage().getHeight(), color);
Blend blend = new Blend(BlendMode.MULTIPLY, new ImageInput(image.getImage()), colorInput);
image.setClip(new ImageView(image.getImage()));
image.setEffect(blend);
image.setCache(true);
}
private static Node getCachedDigitImage(int number, Color color) {
String numberString = String.valueOf(number);
if (numberString.length() == 1) {
char digitToChar = Character.forDigit(number, 10);
ImageView image = new ImageView(digits.get(digitToChar));
applyFontColor(image, color);
return image;
}
HBox layoutPane = new HBox(-4);
for (int i = 0; i < numberString.length(); i++) {
char digitToChar = numberString.charAt(i);
ImageView image = new ImageView(digits.get(digitToChar));
applyFontColor(image, color);
layoutPane.getChildren().add(image);
}
return layoutPane;
}
public static void saveAllDigits() {
Stage stage = new Stage(StageStyle.TRANSPARENT);
DigitTemplate root = new DigitTemplate();
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
SnapshotParameters snapshotParams = new SnapshotParameters();
snapshotParams.setFill(Color.TRANSPARENT);
root.digit.setText("-");
for (int i = 0; i <= 10; i++) {
WritableImage image = root.digit.snapshot(snapshotParams, null);
File file = new File("src/" + IconFactory.RESOURCE_PATH + "/img/common/digits/" + root.digit.getText() + ".png");
try {
ImageIO.write(SwingFXUtils.fromFXImage(image, null), "png", file);
} catch (IOException e) {
e.printStackTrace();
}
root.digit.setText("" + i);
}
stage.close();
}
public static void showPreRenderedDigits(Group group, int number) {
showPreRenderedDigits(group, number, Color.WHITE);
}
public static void showPreRenderedDigits(Group group, int number, Color color) {
group.getChildren().clear();
group.getChildren().add(DigitFactory.getCachedDigitImage(number, color));
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/DigitTemplate.java
================================================
package net.demilich.metastone.gui;
import java.io.IOException;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
public class DigitTemplate extends HBox {
@FXML
public Text digit;
public DigitTemplate() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/DigitTemplate.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/IconFactory.java
================================================
package net.demilich.metastone.gui;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import net.demilich.metastone.game.cards.Rarity;
import net.demilich.metastone.game.entities.heroes.HeroClass;
import net.demilich.metastone.game.heroes.powers.HeroPower;
import net.demilich.metastone.gui.dialog.DialogType;
public class IconFactory {
//public static final String RESOURCE_PATH = "/net/demilich/metastone/resources";
public static final String RESOURCE_PATH = "";
public static Image getClassIcon(HeroClass heroClass) {
String iconPath = RESOURCE_PATH + "/img/classes/";
iconPath += heroClass.toString().toLowerCase();
iconPath += ".png";
return new Image(iconPath);
}
public static Image getDefaultCardBack() {
String iconPath = RESOURCE_PATH + "/img/common/card_back_default.png";
return new Image(iconPath);
}
public static Image getDialogIcon(DialogType dialogType) {
String iconPath = RESOURCE_PATH + "/img/ui/";
switch (dialogType) {
case CONFIRM:
iconPath += "confirm.png";
break;
case ERROR:
iconPath += "error.png";
break;
case INFO:
iconPath += "info.png";
break;
case WARNING:
iconPath += "warning.png";
break;
default:
break;
}
return new Image(iconPath);
}
public static String getHeroIconUrl(HeroClass heroClass) {
String iconPath = RESOURCE_PATH + "/img/heroes/";
switch (heroClass) {
case DRUID:
iconPath += "malfurion";
break;
case HUNTER:
iconPath += "rexxar";
break;
case MAGE:
iconPath += "jaina";
break;
case PALADIN:
iconPath += "uther";
break;
case PRIEST:
iconPath += "anduin";
break;
case ROGUE:
iconPath += "valeera";
break;
case SHAMAN:
iconPath += "thrall";
break;
case WARLOCK:
iconPath += "guldan";
break;
case WARRIOR:
iconPath += "garrosh";
break;
default:
case ANY:
iconPath += "unknown";
break;
}
return iconPath + ".png";
}
public static String getHeroPowerIconUrl(HeroPower heroPower) {
String iconPath = RESOURCE_PATH + "/img/powers/";
switch (heroPower.getHeroClass()) {
case DRUID:
iconPath += "shapeshift";
break;
case HUNTER:
iconPath += "steady_shot";
break;
case MAGE:
iconPath += "fireblast";
break;
case PALADIN:
iconPath += "reinforce";
break;
case PRIEST:
iconPath += "lesser_heal";
break;
case ROGUE:
iconPath += "dagger_mastery";
break;
case SHAMAN:
iconPath += "totemic_call";
break;
case WARLOCK:
iconPath += "life_tap";
break;
case WARRIOR:
iconPath += "armor_up";
break;
default:
iconPath += "unknown";
break;
}
iconPath += ".png";
return iconPath;
}
public static String getImageUrl(String imageName) {
//System.out.println(new File("").getAbsolutePath());
return RESOURCE_PATH + "/img/" + imageName;
}
public static Color getRarityColor(Rarity rarity) {
Color color = Color.BLACK;
switch (rarity) {
case COMMON:
color = Color.WHITE;
break;
case EPIC:
// a335ee
color = Color.rgb(163, 53, 238);
break;
case LEGENDARY:
// ff8000
color = Color.rgb(255, 128, 0);
break;
case RARE:
// 0070dd
color = Color.rgb(0, 112, 221);
break;
default:
color = Color.GRAY;
break;
}
return color;
}
public static Image getSummonHelper() {
String iconPath = RESOURCE_PATH + "/img/common/arrow_down_blue.png";
return new Image(iconPath);
}
public static Image getTargetIcon() {
String iconPath = RESOURCE_PATH + "/img/common/target.png";
return new Image(iconPath);
}
private IconFactory() {
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/autoupdate/AutoUpdateMediator.java
================================================
package net.demilich.metastone.gui.autoupdate;
import java.awt.Desktop;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import org.controlsfx.control.Notifications;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.image.ImageView;
import javafx.util.Duration;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.gui.IconFactory;
import net.demilich.metastone.gui.dialog.DialogType;
import net.demilich.metastone.utils.VersionInfo;
import net.demilich.nittygrittymvc.Mediator;
import net.demilich.nittygrittymvc.interfaces.INotification;
public class AutoUpdateMediator extends Mediator {
public static final String NAME = "AutoUpdateMediator";
private Node view;
public AutoUpdateMediator() {
super(NAME);
}
@Override
public void handleNotification(final INotification notification) {
switch (notification.getId()) {
case CANVAS_CREATED:
view = (Node) notification.getBody();
break;
case NEW_VERSION_AVAILABLE:
VersionInfo versionInfo = (VersionInfo) notification.getBody();
Platform.runLater(() -> showUpdateNotification(versionInfo));
break;
default:
break;
}
}
@Override
public List listNotificationInterests() {
List notificationInterests = new ArrayList();
notificationInterests.add(GameNotification.CANVAS_CREATED);
notificationInterests.add(GameNotification.NEW_VERSION_AVAILABLE);
return notificationInterests;
}
private void showUpdateNotification(VersionInfo versionInfo) {
ImageView icon = new ImageView(IconFactory.getDialogIcon(DialogType.INFO));
icon.setFitWidth(64);
icon.setFitHeight(64);
Notifications.create()
.title("New version available")
.text("MetaStone '" + versionInfo.version + "' is ready for download")
.graphic(icon)
.position(Pos.BOTTOM_CENTER)
.hideAfter(Duration.seconds(5))
.owner(view)
.darkStyle()
.onAction(this::onNotificationClicked)
.show();
}
private void onNotificationClicked(ActionEvent event) {
try {
Desktop.getDesktop().browse(new URI("http://www.demilich.net/metastone/download.html"));
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/autoupdate/CheckForUpdateCommand.java
================================================
package net.demilich.metastone.gui.autoupdate;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import net.demilich.metastone.BuildConfig;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.utils.VersionInfo;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
public class CheckForUpdateCommand extends SimpleCommand {
private static Logger logger = LoggerFactory.getLogger(CheckForUpdateCommand .class);
private static final String MANIFEST_URL = "http://demilich.net/metastone/version/manifest.json";
@Override
public void execute(INotification notification) {
new Thread(this::check).start();
}
private void check() {
try {
RequestConfig globalConfig = RequestConfig.custom().setCircularRedirectsAllowed(true).build();
CloseableHttpClient httpclient = HttpClientBuilder.create().build();
logger.debug("Requesting: " + MANIFEST_URL);
HttpGet httpGet = new HttpGet(MANIFEST_URL);
httpGet.setConfig(globalConfig);
CloseableHttpResponse response = httpclient.execute(httpGet);
try {
HttpEntity entity = response.getEntity();
String htmlContent = EntityUtils.toString(entity);
EntityUtils.consume(entity);
Gson gson = new Gson();
VersionInfo versionInfo = gson.fromJson(htmlContent, VersionInfo.class);
if (versionInfo.isNewerVersionAvailable(BuildConfig.VERSION)) {
logger.debug("Newer version available: {}" + versionInfo.version);
getFacade().sendNotification(GameNotification.NEW_VERSION_AVAILABLE, versionInfo);
} else {
logger.debug("Version up-to-date");
}
} finally {
response.close();
}
} catch (Exception e) {
logger.warn("Auto updater version check failed: " + e.toString());
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/battleofdecks/BattleBatchResult.java
================================================
package net.demilich.metastone.gui.battleofdecks;
import net.demilich.metastone.game.GameContext;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.statistics.GameStatistics;
import net.demilich.metastone.game.statistics.Statistic;
public class BattleBatchResult {
private final int numberOfGames;
private final GameStatistics player1Results = new GameStatistics();
private final GameStatistics player2Results = new GameStatistics();
private int gamesCompleted;
private final Deck deck1;
private final Deck deck2;
private boolean completed;
public BattleBatchResult(Deck deck1, Deck deck2, int numberOfGames) {
this.deck1 = deck1;
this.deck2 = deck2;
this.numberOfGames = numberOfGames;
}
public Deck getDeck1() {
return deck1;
}
public double getDeck1Winrate() {
return getPlayer1Results().getDouble(Statistic.WIN_RATE);
}
public Deck getDeck2() {
return deck2;
}
public double getDeck2Winrate() {
return getPlayer2Results().getDouble(Statistic.WIN_RATE);
}
public int getNumberOfGames() {
return numberOfGames;
}
public GameStatistics getPlayer1Results() {
return player1Results;
}
public GameStatistics getPlayer2Results() {
return player2Results;
}
public double getProgress() {
return gamesCompleted / (double) numberOfGames;
}
public boolean isCompleted() {
return completed;
}
public void onGameEnded(GameContext result) {
getPlayer1Results().merge(result.getPlayer1().getStatistics());
getPlayer2Results().merge(result.getPlayer2().getStatistics());
if (++gamesCompleted == numberOfGames) {
setCompleted(true);
}
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/battleofdecks/BattleBatchResultToken.java
================================================
package net.demilich.metastone.gui.battleofdecks;
import java.io.IOException;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import net.demilich.metastone.gui.IconFactory;
public class BattleBatchResultToken extends BorderPane {
@FXML
private Label deck1Label;
@FXML
private ImageView deck1Icon;
@FXML
private Label deck2Label;
@FXML
private ImageView deck2Icon;
@FXML
private ProgressBar winrate1Bar;
@FXML
private Label winrate1Label;
@FXML
private ProgressBar winrate2Bar;
@FXML
private Label winrate2Label;
@FXML
private Node contentPane;
@FXML
private ProgressIndicator progressIndicator;
public BattleBatchResultToken() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/BattleBatchResultToken.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
contentPane.setOpacity(0.25);
winrate1Bar.setVisible(false);
winrate1Label.setVisible(false);
winrate2Bar.setVisible(false);
winrate2Label.setVisible(false);
}
public void displayBatchResult(BattleBatchResult result) {
if (!result.isCompleted()) {
progressIndicator.setProgress(result.getProgress());
Tooltip.install(this, new Tooltip("In progress\n\n" + result.getDeck1().getName() + "\nVS.\n" + result.getDeck2().getName()));
} else if (contentPane.getOpacity() < 1) {
contentPane.setOpacity(1);
progressIndicator.setVisible(false);
winrate1Bar.setVisible(true);
winrate1Label.setVisible(true);
winrate2Bar.setVisible(true);
winrate2Label.setVisible(true);
Tooltip.install(this, null);
}
deck1Label.setText(result.getDeck1().getName());
deck1Icon.setImage(IconFactory.getClassIcon(result.getDeck1().getHeroClass()));
deck2Label.setText(result.getDeck2().getName());
deck2Icon.setImage(IconFactory.getClassIcon(result.getDeck2().getHeroClass()));
winrate1Bar.setProgress(result.getDeck1Winrate());
winrate1Label.setText(String.format("%.2f", result.getDeck1Winrate() * 100) + "%");
winrate2Bar.setProgress(result.getDeck2Winrate());
winrate2Label.setText(String.format("%.2f", result.getDeck2Winrate() * 100) + "%");
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/battleofdecks/BattleConfig.java
================================================
package net.demilich.metastone.gui.battleofdecks;
import java.util.Collection;
import net.demilich.metastone.game.behaviour.IBehaviour;
import net.demilich.metastone.game.decks.Deck;
public class BattleConfig {
private final int numberOfGames;
private final IBehaviour behaviour;
private final Collection decks;
public BattleConfig(int numberOfGames, IBehaviour behaviour, Collection decks) {
this.numberOfGames = numberOfGames;
this.behaviour = behaviour;
this.decks = decks;
}
public IBehaviour getBehaviour() {
return behaviour;
}
public Collection getDecks() {
return decks;
}
public int getNumberOfGames() {
return numberOfGames;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/battleofdecks/BattleDeckResult.java
================================================
package net.demilich.metastone.gui.battleofdecks;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import net.demilich.metastone.game.statistics.GameStatistics;
import net.demilich.metastone.game.statistics.Statistic;
public class BattleDeckResult {
private final StringProperty deckName = new SimpleStringProperty();
private final ObjectProperty deckStatistics = new SimpleObjectProperty<>();
private final DoubleProperty winRate = new SimpleDoubleProperty();
public BattleDeckResult(String deckName, GameStatistics deckStatistics) {
setDeckName(deckName);
setDeckStatistics(deckStatistics);
setWinRate(deckStatistics.getDouble(Statistic.WIN_RATE));
}
public final StringProperty deckNameProperty() {
return this.deckName;
}
public final ObjectProperty deckStatisticsProperty() {
return this.deckStatistics;
}
public final String getDeckName() {
return this.deckNameProperty().get();
}
public final GameStatistics getDeckStatistics() {
return this.deckStatisticsProperty().get();
}
public final double getWinRate() {
return this.winRateProperty().get();
}
public final void setDeckName(final String deckName) {
this.deckNameProperty().set(deckName);
}
public final void setDeckStatistics(final GameStatistics deckStatistics) {
this.deckStatisticsProperty().set(deckStatistics);
}
public final void setWinRate(final double winRate) {
this.winRateProperty().set(winRate);
}
public final DoubleProperty winRateProperty() {
return this.winRate;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/battleofdecks/BattleOfDecksConfigView.java
================================================
package net.demilich.metastone.gui.battleofdecks;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.scene.layout.BorderPane;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.behaviour.IBehaviour;
import net.demilich.metastone.game.behaviour.PlayRandomBehaviour;
import net.demilich.metastone.game.behaviour.threat.GameStateValueBehaviour;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.DeckFormat;
import net.demilich.metastone.game.entities.heroes.HeroClass;
import net.demilich.metastone.gui.common.BehaviourStringConverter;
import net.demilich.metastone.gui.common.DeckStringConverter;
public class BattleOfDecksConfigView extends BorderPane {
@FXML
private ComboBox numberOfGamesBox;
@FXML
private ComboBox behaviourBox;
@FXML
private ListView selectedDecksListView;
@FXML
private ListView availableDecksListView;
@FXML
private Button addButton;
@FXML
private Button removeButton;
@FXML
private Button startButton;
@FXML
private Button backButton;
public BattleOfDecksConfigView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/BattleOfDecksConfigView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
setupBehaviourBox();
setupNumberOfGamesBox();
selectedDecksListView.setCellFactory(TextFieldListCell.forListView(new DeckStringConverter()));
selectedDecksListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
availableDecksListView.setCellFactory(TextFieldListCell.forListView(new DeckStringConverter()));
availableDecksListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
addButton.setOnAction(this::handleAddButton);
removeButton.setOnAction(this::handleRemoveButton);
backButton.setOnAction(event -> NotificationProxy.sendNotification(GameNotification.MAIN_MENU));
startButton.setOnAction(this::handleStartButton);
}
private void handleAddButton(ActionEvent event) {
Collection selectedDecks = availableDecksListView.getSelectionModel().getSelectedItems();
selectedDecksListView.getItems().addAll(selectedDecks);
availableDecksListView.getItems().removeAll(selectedDecks);
}
private void handleRemoveButton(ActionEvent event) {
Collection selectedDecks = selectedDecksListView.getSelectionModel().getSelectedItems();
availableDecksListView.getItems().addAll(selectedDecks);
selectedDecksListView.getItems().removeAll(selectedDecks);
}
private void handleStartButton(ActionEvent event) {
int numberOfGames = numberOfGamesBox.getSelectionModel().getSelectedItem();
IBehaviour behaviour = behaviourBox.getSelectionModel().getSelectedItem();
Collection decks = selectedDecksListView.getItems();
BattleConfig battleConfig = new BattleConfig(numberOfGames, behaviour, decks);
NotificationProxy.sendNotification(GameNotification.COMMIT_BATTLE_OF_DECKS_CONFIG, battleConfig);
}
public void injectDecks(List decks) {
selectedDecksListView.getItems().clear();
ObservableList validDecks = FXCollections.observableArrayList();
for (Deck deck : decks) {
if (deck.getHeroClass() == HeroClass.MAGE) {
continue;
}
}
availableDecksListView.getItems().setAll(validDecks);
}
private void setupBehaviourBox() {
behaviourBox.setConverter(new BehaviourStringConverter());
behaviourBox.getItems().setAll(new GameStateValueBehaviour(), new PlayRandomBehaviour());
behaviourBox.getSelectionModel().selectFirst();
}
private void setupNumberOfGamesBox() {
ObservableList numberOfGamesEntries = FXCollections.observableArrayList();
numberOfGamesEntries.add(1);
numberOfGamesEntries.add(10);
numberOfGamesEntries.add(100);
numberOfGamesEntries.add(1000);
numberOfGamesBox.setItems(numberOfGamesEntries);
numberOfGamesBox.getSelectionModel().select(2);
}
public void injectDeckFormats(List deckFormats) {
// selectedDeckFormatsListView.getItems().clear();
// ObservableList validDeckFormats = FXCollections.observableArrayList();
// availableDeckFormatsListView.getItems().setAll(validDeckFormats);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/battleofdecks/BattleOfDecksMediator.java
================================================
package net.demilich.metastone.gui.battleofdecks;
import java.util.ArrayList;
import java.util.List;
import net.demilich.nittygrittymvc.Mediator;
import net.demilich.nittygrittymvc.interfaces.INotification;
import javafx.application.Platform;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.DeckFormat;
public class BattleOfDecksMediator extends Mediator {
public final static String NAME = "BattleOfDecksMediator";
private final BattleOfDecksConfigView configView;
private final BattleOfDecksResultView resultView;
public BattleOfDecksMediator() {
super(NAME);
configView = new BattleOfDecksConfigView();
resultView = new BattleOfDecksResultView();
}
@SuppressWarnings("unchecked")
@Override
public void handleNotification(final INotification notification) {
switch (notification.getId()) {
case REPLY_DECKS:
configView.injectDecks((List) notification.getBody());
break;
case REPLY_DECK_FORMATS:
configView.injectDeckFormats((List) notification.getBody());
break;
case BATTLE_OF_DECKS_PROGRESS_UPDATE:
final BattleResult result = (BattleResult) notification.getBody();
Platform.runLater(() -> resultView.updateResults(result));
break;
case COMMIT_BATTLE_OF_DECKS_CONFIG:
sendNotification(GameNotification.SHOW_VIEW, resultView);
break;
default:
break;
}
}
@Override
public List listNotificationInterests() {
List notificationInterests = new ArrayList();
notificationInterests.add(GameNotification.REPLY_DECKS);
notificationInterests.add(GameNotification.REPLY_DECK_FORMATS);
notificationInterests.add(GameNotification.BATTLE_OF_DECKS_PROGRESS_UPDATE);
notificationInterests.add(GameNotification.COMMIT_BATTLE_OF_DECKS_CONFIG);
return notificationInterests;
}
@Override
public void onRegister() {
sendNotification(GameNotification.SHOW_VIEW, configView);
sendNotification(GameNotification.REQUEST_DECKS);
sendNotification(GameNotification.REQUEST_DECK_FORMATS);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/battleofdecks/BattleOfDecksResultView.java
================================================
package net.demilich.metastone.gui.battleofdecks;
import java.io.IOException;
import java.util.HashMap;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
public class BattleOfDecksResultView extends BorderPane {
@FXML
private FlowPane batchResultPane;
@FXML
private TableView rankingTable;
@FXML
private Button backButton;
private final HashMap tokenMap = new HashMap<>();
@SuppressWarnings("unchecked")
public BattleOfDecksResultView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/BattleOfDecksResultView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
TableColumn nameColumn = new TableColumn<>("Deck name");
nameColumn.setPrefWidth(200);
TableColumn winRateColumn = new TableColumn<>("Win rate");
winRateColumn.setPrefWidth(150);
nameColumn.setCellValueFactory(new PropertyValueFactory("deckName"));
winRateColumn.setCellValueFactory(new PropertyValueFactory("winRate"));
winRateColumn.setCellFactory(new Callback, TableCell>() {
public TableCell call(TableColumn p) {
TableCell cell = new TableCell() {
private final Label label = new Label();
private final ProgressBar progressBar = new ProgressBar();
private final StackPane stackPane = new StackPane();
{
label.getStyleClass().setAll("progress-text");
stackPane.setAlignment(Pos.CENTER);
stackPane.getChildren().setAll(progressBar, label);
setGraphic(stackPane);
}
@Override
protected void updateItem(Double winrate, boolean empty) {
super.updateItem(winrate, empty);
if (winrate == null || empty) {
setGraphic(null);
return;
}
progressBar.setProgress(winrate);
label.setText(String.format("%.2f", winrate * 100) + "%");
setGraphic(stackPane);
}
};
return cell;
}
});
rankingTable.getColumns().setAll(nameColumn, winRateColumn);
rankingTable.getColumns().get(1).setSortType(SortType.DESCENDING);
backButton.setOnAction(event -> NotificationProxy.sendNotification(GameNotification.MAIN_MENU));
}
@SuppressWarnings("unchecked")
public void updateResults(BattleResult result) {
for (BattleBatchResult batchResult : result.getBatchResults()) {
if (!tokenMap.containsKey(batchResult)) {
BattleBatchResultToken token = new BattleBatchResultToken();
tokenMap.put(batchResult, token);
batchResultPane.getChildren().add(token);
}
BattleBatchResultToken batchResultToken = tokenMap.get(batchResult);
batchResultToken.displayBatchResult(batchResult);
}
rankingTable.getItems().setAll(result.getDeckResults());
rankingTable.getSortOrder().setAll(rankingTable.getColumns().get(1));
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/battleofdecks/BattleResult.java
================================================
package net.demilich.metastone.gui.battleofdecks;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import net.demilich.metastone.game.GameContext;
import net.demilich.metastone.game.Player;
import net.demilich.metastone.game.statistics.GameStatistics;
public class BattleResult {
private final int numberOfGames;
private final HashMap deckResults = new HashMap();
private final List batchResults = new ArrayList();
public BattleResult(int numberOfGames) {
this.numberOfGames = numberOfGames;
}
public void addBatchResult(BattleBatchResult batchResult) {
synchronized (batchResults) {
batchResults.add(batchResult);
}
}
public List getBatchResults() {
synchronized (batchResults) {
return new ArrayList(batchResults);
}
}
public List getDeckResults() {
List resultList = new ArrayList();
synchronized (deckResults) {
for (String deckName : deckResults.keySet()) {
BattleDeckResult deckResult = new BattleDeckResult(deckName, deckResults.get(deckName));
resultList.add(deckResult);
}
}
return resultList;
}
public int getNumberOfGames() {
return numberOfGames;
}
public void onGameEnded(GameContext result) {
for (Player player : result.getPlayers()) {
updateStats(player);
}
}
private void updateStats(Player player) {
String deckName = player.getDeckName();
synchronized (deckResults) {
if (!deckResults.containsKey(deckName)) {
deckResults.put(deckName, new GameStatistics());
}
GameStatistics stats = deckResults.get(deckName);
stats.merge(player.getStatistics());
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/battleofdecks/StartBattleOfDecksCommand.java
================================================
package net.demilich.metastone.gui.battleofdecks;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.GameContext;
import net.demilich.metastone.game.Player;
import net.demilich.metastone.game.behaviour.IBehaviour;
import net.demilich.metastone.game.cards.CardSet;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.DeckFormat;
import net.demilich.metastone.game.logic.GameLogic;
import net.demilich.metastone.game.gameconfig.PlayerConfig;
public class StartBattleOfDecksCommand extends SimpleCommand {
private class PlayGameTask implements Callable {
private final PlayerConfig player1Config;
private final PlayerConfig player2Config;
private final BattleBatchResult batchResult;
public PlayGameTask(Deck deck1, Deck deck2, IBehaviour behaviour, BattleBatchResult batchResult) {
this.player1Config = new PlayerConfig(deck1, behaviour);
player1Config.setName("Player 1");
this.player2Config = new PlayerConfig(deck2, behaviour);
player2Config.setName("Player 2");
this.batchResult = batchResult;
}
@Override
public Void call() throws Exception {
Player player1 = new Player(player1Config);
Player player2 = new Player(player2Config);
DeckFormat deckFormat = new DeckFormat();
for (CardSet set : CardSet.values()) {
deckFormat.addSet(set);
}
GameContext newGame = new GameContext(player1, player2, new GameLogic(), deckFormat);
newGame.play();
batchResult.onGameEnded(newGame);
result.onGameEnded(newGame);
periodicUpdate();
newGame.dispose();
return null;
}
}
private static Logger logger = LoggerFactory.getLogger(StartBattleOfDecksCommand.class);
private BattleResult result;
private long lastUpdate;
@Override
public void execute(INotification notification) {
BattleConfig battleConfig = (BattleConfig) notification.getBody();
result = new BattleResult(battleConfig.getNumberOfGames());
Thread t = new Thread(new Runnable() {
@Override
public void run() {
logger.info("Battle of Decks started");
ExecutorService executor = Executors.newWorkStealingPool();
List> futures = new ArrayList>();
HashSet processedDecks = new HashSet<>();
for (Deck deck1 : battleConfig.getDecks()) {
processedDecks.add(deck1);
for (Deck deck2 : battleConfig.getDecks()) {
if (processedDecks.contains(deck2)) {
continue;
}
BattleBatchResult batchResult = new BattleBatchResult(deck1, deck2, battleConfig.getNumberOfGames());
result.addBatchResult(batchResult);
for (int i = 0; i < battleConfig.getNumberOfGames(); i++) {
PlayGameTask task = new PlayGameTask(deck1, deck2, battleConfig.getBehaviour(), batchResult);
Future future = executor.submit(task);
futures.add(future);
}
}
}
executor.shutdown();
boolean completed = false;
while (!completed) {
completed = true;
for (Future future : futures) {
if (!future.isDone()) {
completed = false;
continue;
}
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
logger.error(ExceptionUtils.getStackTrace(e));
e.printStackTrace();
System.exit(-1);
}
}
futures.removeIf(future -> future.isDone());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// getFacade().sendNotification(GameNotification.SIMULATION_RESULT,
// result);
logger.info("Battle of Decks finished");
}
});
t.setDaemon(true);
t.start();
}
private void periodicUpdate() {
if (System.currentTimeMillis() - lastUpdate > 1000) {
sendNotification(GameNotification.BATTLE_OF_DECKS_PROGRESS_UPDATE, result);
lastUpdate = System.currentTimeMillis();
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/cards/CardProxy.java
================================================
package net.demilich.metastone.gui.cards;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.cards.CardCatalogue;
import net.demilich.metastone.game.cards.CardParseException;
import net.demilich.nittygrittymvc.Proxy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class CardProxy extends Proxy {
public final static String NAME = "CardProxy";
private static Logger logger = LoggerFactory.getLogger(CardProxy.class);
public CardProxy() {
super(NAME);
try {
// ensure user's personal cards dir exists
Files.createDirectories(Paths.get(CardCatalogue.CARDS_FOLDER_PATH));
// ensure cards have been copied to ~/metastone/cards
CardCatalogue.copyCardsFromResources();
CardCatalogue.loadCards();
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (IOException e) {
logger.error("Trouble creating " + Paths.get(CardCatalogue.CARDS_FOLDER_PATH));
e.printStackTrace();
} catch (CardParseException cpe) {
getFacade().sendNotification(GameNotification.CARD_PARSE_ERROR, cpe.getMessage());
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/cards/CardToken.java
================================================
package net.demilich.metastone.gui.cards;
import java.io.IOException;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Group;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import net.demilich.metastone.game.GameContext;
import net.demilich.metastone.game.Player;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.cards.CardType;
import net.demilich.metastone.game.cards.MinionCard;
import net.demilich.metastone.game.cards.Rarity;
import net.demilich.metastone.game.cards.WeaponCard;
import net.demilich.metastone.gui.DigitFactory;
import net.demilich.metastone.gui.IconFactory;
public class CardToken extends BorderPane {
@FXML
protected Group manaCostAnchor;
@FXML
protected Label nameLabel;
@FXML
protected Label descriptionLabel;
@FXML
protected Group attackAnchor;
@FXML
protected Group hpAnchor;
@FXML
protected ImageView attackIcon;
@FXML
protected ImageView hpIcon;
@FXML
protected Circle rarityGem;
private double baseRarityGemSize;
protected Card card;
protected CardToken(String fxml) {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/" + fxml));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
baseRarityGemSize = rarityGem.getRadius();
}
public Card getCard() {
return card;
}
public void setCard(Card card) {
setCard(null, card, null);
}
public void setCard(GameContext context, Card card, Player player) {
this.card = card;
nameLabel.setText(card.getName());
setRarity(card.getRarity());
if (context != null || player != null) {
int modifiedManaCost = context.getLogic().getModifiedManaCost(player, card);
setScoreValueLowerIsBetter(manaCostAnchor, modifiedManaCost, card.getBaseManaCost());
} else {
setScoreValue(manaCostAnchor, card.getBaseManaCost());
}
boolean isMinionOrWeaponCard = card.getCardType().isCardType(CardType.MINION) || card.getCardType().isCardType(CardType.WEAPON);
attackAnchor.setVisible(isMinionOrWeaponCard);
hpAnchor.setVisible(isMinionOrWeaponCard);
attackIcon.setVisible(isMinionOrWeaponCard);
hpIcon.setVisible(isMinionOrWeaponCard);
if (card.getCardType().isCardType(CardType.MINION)) {
MinionCard minionCard = (MinionCard) card;
setScoreValue(attackAnchor, minionCard.getAttack() + minionCard.getBonusAttack(), minionCard.getBaseAttack());
setScoreValue(hpAnchor, minionCard.getHp() + minionCard.getBonusHp(), minionCard.getBaseHp());
} else if (card.getCardType().isCardType(CardType.WEAPON)) {
WeaponCard weaponCard = (WeaponCard) card;
setScoreValue(attackAnchor, weaponCard.getDamage() + weaponCard.getBonusDamage(), weaponCard.getBaseDamage());
setScoreValue(hpAnchor, weaponCard.getDurability() + weaponCard.getBonusDurability(), weaponCard.getBaseDurability());
}
}
public void setNonCard(String name, String description) {
nameLabel.setText(name);
descriptionLabel.setText(description);
setRarity(Rarity.FREE);
manaCostAnchor.setVisible(false);
attackAnchor.setVisible(false);
hpAnchor.setVisible(false);
attackIcon.setVisible(false);
hpIcon.setVisible(false);
}
private void setRarity(Rarity rarity) {
rarityGem.setFill(IconFactory.getRarityColor(rarity));
rarityGem.setVisible(rarity != Rarity.FREE);
rarityGem.setRadius(rarity == Rarity.LEGENDARY ? baseRarityGemSize * 1.5 : baseRarityGemSize);
}
protected void setScoreValue(Group group, int value) {
setScoreValue(group, value, value);
}
protected void setScoreValue(Group group, int value, int baseValue) {
Color color = Color.WHITE;
if (value > baseValue) {
color = Color.GREEN;
}
DigitFactory.showPreRenderedDigits(group, value, color);
}
protected void setScoreValue(Group group, int value, int baseValue, int maxValue) {
Color color = Color.WHITE;
if (value < maxValue) {
color = Color.RED;
} else if (value > baseValue) {
color = Color.GREEN;
}
DigitFactory.showPreRenderedDigits(group, value, color);
}
private void setScoreValueLowerIsBetter(Group group, int value, int baseValue) {
Color color = Color.WHITE;
if (value < baseValue) {
color = Color.GREEN;
} else if (value > baseValue) {
color = Color.RED;
}
DigitFactory.showPreRenderedDigits(group, value, color);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/cards/CardTokenFactory.java
================================================
package net.demilich.metastone.gui.cards;
import java.util.ArrayList;
import java.util.List;
import net.demilich.metastone.game.GameContext;
import net.demilich.metastone.game.Player;
import net.demilich.metastone.game.cards.Card;
public class CardTokenFactory {
private static final int HAND_CARDS = 10;
private List cachedHandCards = new ArrayList(HAND_CARDS);
public CardTokenFactory() {
for (int i = 0; i < HAND_CARDS; i++) {
cachedHandCards.add(new HandCard());
}
}
public CardToken createHandCard(GameContext context, Card card, Player player) {
HandCard handCard = getHandCard();
handCard.setCard(context, card, player);
return handCard;
}
private HandCard getHandCard() {
for (HandCard handCard : cachedHandCards) {
if (handCard.getParent() == null) {
return handCard;
}
}
return new HandCard();
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/cards/CardTooltip.java
================================================
package net.demilich.metastone.gui.cards;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import net.demilich.metastone.game.Attribute;
import net.demilich.metastone.game.GameContext;
import net.demilich.metastone.game.Player;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.entities.minions.Race;
public class CardTooltip extends CardToken {
@FXML
private Label raceLabel;
public CardTooltip() {
super("CardTooltip.fxml");
}
@Override
public void setCard(GameContext context, Card card, Player player) {
super.setCard(context, card, player);
descriptionLabel.setText(card.getDescription());
if (!card.hasAttribute(Attribute.RACE) || card.getAttribute(Attribute.RACE) == Race.NONE) {
raceLabel.setVisible(false);
} else {
raceLabel.setText(card.getAttribute(Attribute.RACE).toString());
raceLabel.setVisible(true);
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/cards/HandCard.java
================================================
package net.demilich.metastone.gui.cards;
import javafx.fxml.FXML;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.Pane;
import net.demilich.metastone.game.GameContext;
import net.demilich.metastone.game.Player;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.gui.IconFactory;
public class HandCard extends CardToken {
@FXML
private Pane topPane;
@FXML
private Pane centerPane;
@FXML
private Pane bottomPane;
private CardTooltip tooltipContent;
private Tooltip tooltip;
public HandCard() {
super("HandCard.fxml");
hideCard(true);
}
private void hideCard(boolean hide) {
topPane.setVisible(!hide);
centerPane.setVisible(!hide);
bottomPane.setVisible(!hide);
if (hide) {
BackgroundSize size = new BackgroundSize(getWidth(), getHeight(), false, false, true, false);
BackgroundImage image = new BackgroundImage(IconFactory.getDefaultCardBack(), BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT, BackgroundPosition.CENTER, size);
Background background = new Background(image);
setBackground(background);
}
}
@Override
public void setCard(GameContext context, Card card, Player player) {
super.setCard(context, card, player);
if (tooltipContent == null) {
tooltip = new Tooltip();
tooltipContent = new CardTooltip();
tooltipContent.setCard(context, card, player);
tooltip.setGraphic(tooltipContent);
Tooltip.install(this, tooltip);
} else {
tooltipContent.setCard(context, card, player);
}
hideCard(player.hideCards());
if (player.hideCards()) {
Tooltip.uninstall(this, tooltip);
tooltipContent = null;
tooltip = null;
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/common/BehaviourStringConverter.java
================================================
package net.demilich.metastone.gui.common;
import javafx.util.StringConverter;
import net.demilich.metastone.game.behaviour.IBehaviour;
public class BehaviourStringConverter extends StringConverter {
@Override
public IBehaviour fromString(String string) {
return null;
}
@Override
public String toString(IBehaviour behaviour) {
return behaviour.getName();
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/common/CardSetStringConverter.java
================================================
package net.demilich.metastone.gui.common;
import javafx.util.StringConverter;
import net.demilich.metastone.game.cards.CardSet;
public class CardSetStringConverter extends StringConverter {
@Override
public CardSet fromString(String string) {
return null;
}
@Override
public String toString(CardSet object) {
return object.toString();
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/common/ComboBoxKeyHandler.java
================================================
package net.demilich.metastone.gui.common;
import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.event.EventHandler;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class ComboBoxKeyHandler implements EventHandler {
private static final long WORD_DELAY = 2000;
private String s;
private final ComboBox box;
private long lastKeyPress;
public ComboBoxKeyHandler(ComboBox box) {
this.box = box;
s = "";
}
@Override
public void handle(KeyEvent event) {
if (System.currentTimeMillis() - WORD_DELAY > lastKeyPress) {
s = "";
}
// handle non alphanumeric keys like backspace, delete etc
if (event.getCode() == KeyCode.BACK_SPACE && s.length() > 0)
s = s.substring(0, s.length() - 1);
else
s += event.getText();
lastKeyPress = System.currentTimeMillis();
if (s.length() == 0) {
select(0);
return;
}
for (T item : box.getItems()) {
String name = box.getConverter().toString(item).toLowerCase();
if (name.startsWith(s)) {
select(item);
return;
}
}
// nothing found, reset search string
s = "";
}
private void select(int index) {
select(box.getItems().get(index));
}
@SuppressWarnings("rawtypes")
private void select(T item) {
box.getSelectionModel().select(item);
ListView lv = ((ComboBoxListViewSkin) box.getSkin()).getListView();
lv.scrollTo(lv.getSelectionModel().getSelectedIndex());
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/common/DeckFormatStringConverter.java
================================================
package net.demilich.metastone.gui.common;
import javafx.util.StringConverter;
import net.demilich.metastone.game.decks.DeckFormat;
public class DeckFormatStringConverter extends StringConverter {
@Override
public DeckFormat fromString(String arg0) {
return null;
}
@Override
public String toString(DeckFormat format) {
return format.getName();
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/common/DeckStringConverter.java
================================================
package net.demilich.metastone.gui.common;
import javafx.util.StringConverter;
import net.demilich.metastone.game.decks.Deck;
public class DeckStringConverter extends StringConverter {
@Override
public Deck fromString(String arg0) {
return null;
}
@Override
public String toString(Deck deck) {
return deck.getName();
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/common/HeroStringConverter.java
================================================
package net.demilich.metastone.gui.common;
import javafx.util.StringConverter;
import net.demilich.metastone.game.cards.HeroCard;
public class HeroStringConverter extends StringConverter {
@Override
public HeroCard fromString(String arg0) {
return null;
}
@Override
public String toString(HeroCard hero) {
return hero.getHeroClass().toString();
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/common/IntegerTextField.java
================================================
package net.demilich.metastone.gui.common;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class IntegerTextField extends RestrictedTextField {
private final IntegerProperty valueProperty = new SimpleIntegerProperty();
public IntegerTextField(int maxLength) {
setRestrict("\\d*");
setMaxLength(maxLength);
}
public int getIntValue() {
return valueProperty().get();
}
public void setIntValue(int value) {
setText(String.valueOf(value));
}
@Override
protected void validInput(String validInput) {
valueProperty().set(validInput.length() > 0 ? Integer.parseInt(validInput) : 0);
if (validInput.length() == 0) {
setIntValue(0);
}
}
public IntegerProperty valueProperty() {
return valueProperty;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/common/RestrictedTextField.java
================================================
package net.demilich.metastone.gui.common;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextField;
/**
* A text field, which restricts the user's input.
*
* @author Christian Schudt
*/
public class RestrictedTextField extends TextField {
private StringProperty restrict = new SimpleStringProperty();
private IntegerProperty maxLength = new SimpleIntegerProperty(-1);
public RestrictedTextField() {
textProperty().addListener(new ChangeListener() {
private boolean ignore;
@Override
public void changed(ObservableValue extends String> observableValue, String s, String s1) {
if (ignore)
return;
if (maxLength.get() > -1 && s1.length() > maxLength.get()) {
ignore = true;
setText(s1.substring(0, maxLength.get()));
validInput(getText());
ignore = false;
return;
}
if (restrict.get() != null && !restrict.get().equals("") && !s1.matches(restrict.get())) {
ignore = true;
setText(s);
ignore = false;
return;
}
validInput(getText());
}
});
}
public int getMaxLength() {
return maxLength.get();
}
public String getRestrict() {
return restrict.get();
}
public IntegerProperty maxLengthProperty() {
return maxLength;
}
public StringProperty restrictProperty() {
return restrict;
}
/**
* Sets the max length of the text field.
*
* @param maxLength
* The max length.
*/
public void setMaxLength(int maxLength) {
this.maxLength.set(maxLength);
}
/**
* Sets a regular expression character class which restricts the user input.
*
* E.g. [0-9] only allows numeric values.
*
* @param restrict
* The regular expression.
*/
public void setRestrict(String restrict) {
this.restrict.set(restrict);
}
protected void validInput(String validInput) {
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/AddCardToDeckCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.cards.Card;
public class AddCardToDeckCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
Card card = (Card) notification.getBody();
if (deckProxy.addCardToDeck(card)) {
getFacade().sendNotification(GameNotification.ACTIVE_DECK_CHANGED, deckProxy.getActiveDeck());
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/CardEntry.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.IOException;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import net.demilich.metastone.game.cards.Card;
public class CardEntry extends HBox {
@FXML
private Label cardNameLabel;
@FXML
private Text manaCostText;
@FXML
private Text countText;
private int stack;
private Card card;
public CardEntry() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/CardEntry.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
setCache(true);
}
public void addCard(Card card) {
this.card = card;
cardNameLabel.setText(card.getName());
manaCostText.setText(String.valueOf(card.getBaseManaCost()));
stack++;
countText.setText(String.valueOf(stack));
countText.setVisible(stack > 1);
}
public Card getCard() {
return card;
}
public void resetStackCount() {
stack = 0;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/CardEntryFactory.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.util.ArrayList;
import java.util.List;
import net.demilich.metastone.game.cards.Card;
public class CardEntryFactory {
private static final int CARD_ENTRIES = 10;
private List cachedCardEntries = new ArrayList(CARD_ENTRIES);
public CardEntryFactory() {
for (int i = 0; i < CARD_ENTRIES; i++) {
cachedCardEntries.add(new CardEntry());
}
}
public CardEntry createCardEntry(Card card) {
CardEntry cardEntry = getCardEntry();
cardEntry.resetStackCount();
cardEntry.addCard(card);
return cardEntry;
}
private CardEntry getCardEntry() {
for (CardEntry handCard : cachedCardEntries) {
if (handCard.getParent() == null) {
return handCard;
}
}
return new CardEntry();
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/CardFilter.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import net.demilich.metastone.game.cards.CardSet;
import net.demilich.metastone.game.decks.DeckFormat;
public class CardFilter {
private final String text;
private final CardSet set;
private final DeckFormat format;
public CardFilter(String text, CardSet set, DeckFormat format) {
this.text = text;
this.set = set;
this.format = format;
}
public DeckFormat getFormat() {
return format;
}
public CardSet getSet() {
return set;
}
public String getText() {
return text;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/CardFilterView.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.cards.CardSet;
import net.demilich.metastone.game.decks.DeckFormat;
import net.demilich.metastone.gui.common.CardSetStringConverter;
import net.demilich.metastone.gui.common.DeckFormatStringConverter;
public class CardFilterView extends HBox {
@FXML
private TextField searchField;
@FXML
private ComboBox cardSetBox;
@FXML
private ComboBox deckFormatBox;
private List deckFormats = new ArrayList();
public CardFilterView(List deckFormats) {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/CardFilterView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
searchField.textProperty().addListener(this::textChanged);
deckFormatBox.setConverter(new DeckFormatStringConverter());
DeckFormat deckFormat = new DeckFormat();
deckFormat.setName("DECK FORMAT");
deckFormats.add(0, deckFormat);
deckFormatBox.setItems(FXCollections.observableArrayList(deckFormats));
deckFormatBox.getSelectionModel().selectFirst();
deckFormatBox.valueProperty().addListener(this::formatChanged);
cardSetBox.setConverter(new CardSetStringConverter());
cardSetBox.setItems(FXCollections.observableArrayList(CardSet.values()));
cardSetBox.getSelectionModel().selectFirst();
cardSetBox.valueProperty().addListener(this::setChanged);
}
private void filterChanged() {
DeckFormat deckFormat = null;
if (!deckFormatBox.getSelectionModel().isSelected(0)) {
deckFormat = deckFormatBox.getSelectionModel().getSelectedItem();
}
NotificationProxy.sendNotification(GameNotification.FILTER_CARDS,
new CardFilter(searchField.getText(), cardSetBox.getSelectionModel().getSelectedItem(), deckFormat));
}
private void formatChanged(ObservableValue extends DeckFormat> observable, DeckFormat oldValue, DeckFormat newValue) {
CardSet set = cardSetBox.getSelectionModel().getSelectedItem();
if (deckFormatBox.getSelectionModel().isSelected(0)) {
cardSetBox.setItems(FXCollections.observableArrayList(CardSet.values()));
} else {
List sets = newValue.getCardSets();
sets.add(0, CardSet.ANY);
cardSetBox.setItems(FXCollections.observableArrayList(sets));
}
if (!deckFormatBox.getSelectionModel().isSelected(0) && !set.equals(CardSet.ANY) && !newValue.isInFormat(set)) {
cardSetBox.getSelectionModel().selectFirst();
} else {
cardSetBox.getSelectionModel().select(set);
}
filterChanged();
}
public void injectDeckFormats(List deckFormats) {
this.deckFormats.addAll(deckFormats);
}
private void setChanged(ObservableValue extends CardSet> observable, CardSet oldValue, CardSet newValue) {
filterChanged();
}
private void textChanged(ObservableValue extends String> observable, String oldValue, String newValue) {
filterChanged();
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/CardListView.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.util.HashMap;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.decks.Deck;
public class CardListView extends VBox implements EventHandler {
private final HashMap existingCardEntries = new HashMap();
private final CardEntryFactory cardEntryFactory = new CardEntryFactory();
public CardListView() {
super(2);
this.setAlignment(Pos.TOP_LEFT);
this.setPrefSize(240, USE_COMPUTED_SIZE);
}
private void clearChildren() {
for (Node child : getChildren()) {
child.removeEventHandler(MouseEvent.MOUSE_CLICKED, this);
}
getChildren().clear();
}
public void displayDeck(Deck deck) {
existingCardEntries.clear();
clearChildren();
for (Card card : deck.getCards()) {
String cardId = card.getCardId();
CardEntry cardEntry = null;
if (existingCardEntries.containsKey(cardId)) {
cardEntry = existingCardEntries.get(cardId);
cardEntry.addCard(card);
} else {
cardEntry = cardEntryFactory.createCardEntry(card);
cardEntry.addEventHandler(MouseEvent.MOUSE_CLICKED, this);
getChildren().add(cardEntry);
existingCardEntries.put(cardId, cardEntry);
}
}
}
@Override
public void handle(MouseEvent event) {
Card card = null;
for (CardEntry cardEntry : existingCardEntries.values()) {
if (event.getSource() == cardEntry) {
card = cardEntry.getCard();
break;
}
}
if (card != null) {
NotificationProxy.sendNotification(GameNotification.REMOVE_CARD_FROM_DECK, card);
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/CardView.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.gui.cards.CardTooltip;
public class CardView extends BorderPane implements EventHandler {
@FXML
private Pane contentPane;
@FXML
private Button previousButton;
@FXML
private Button nextButton;
@FXML
private Label pageLabel;
private int offset;
private final int rows = 4;
private final int columns = 2;
private final int cardDisplayCount = rows * columns;
private List cards;
private final List cardWidgets = new ArrayList(cardDisplayCount);
public CardView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/CardView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
setupCardWidgets();
previousButton.setOnAction(actionEvent -> changeOffset(-cardDisplayCount));
nextButton.setOnAction(actionEvent -> changeOffset(+cardDisplayCount));
setCache(true);
}
private void changeOffset(int delta) {
int newOffset = offset + delta;
if (newOffset < 0 || newOffset >= cards.size()) {
return;
}
offset += delta;
displayCurrentPage();
}
public void displayCards(List cards) {
this.cards = cards;
offset = 0;
displayCurrentPage();
}
private void displayCurrentPage() {
int lastIndex = Math.min(cards.size(), offset + cardDisplayCount);
updatePageLabel();
for (CardTooltip CardTooltip : cardWidgets) {
CardTooltip.setVisible(false);
}
int widgetIndex = 0;
for (int i = offset; i < lastIndex; i++) {
Card card = cards.get(i);
CardTooltip cardWidget = cardWidgets.get(widgetIndex++);
cardWidget.setCard(card);
cardWidget.setVisible(true);
}
}
@Override
public void handle(MouseEvent event) {
CardTooltip source = (CardTooltip) event.getSource();
Card card = source.getCard();
NotificationProxy.sendNotification(GameNotification.ADD_CARD_TO_DECK, card);
}
private void setupCardWidgets() {
for (int i = 0; i < cardDisplayCount; i++) {
CardTooltip cardWidget = new CardTooltip();
cardWidget.addEventHandler(MouseEvent.MOUSE_CLICKED, this);
cardWidget.setScaleX(0.95);
cardWidget.setScaleY(0.95);
cardWidget.setScaleZ(0.95);
contentPane.getChildren().add(cardWidget);
cardWidgets.add(cardWidget);
}
}
private void updatePageLabel() {
int totalPages = (int) Math.ceil(cards.size() / (double) cardDisplayCount);
int currentPage = (int) Math.ceil(offset / (double) cardDisplayCount) + 1;
pageLabel.setText(currentPage + "/" + totalPages);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/ChangeDeckNameCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
public class ChangeDeckNameCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
String newDeckName = (String) notification.getBody();
deckProxy.getActiveDeck().setName(newDeckName);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/ChooseClassView.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.IOException;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.BorderPane;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.MetaDeck;
import net.demilich.metastone.game.entities.heroes.HeroClass;
public class ChooseClassView extends BorderPane implements EventHandler {
@FXML
private Button warriorButton;
@FXML
private Button paladinButton;
@FXML
private Button druidButton;
@FXML
private Button rogueButton;
@FXML
private Button warlockButton;
@FXML
private Button hunterButton;
@FXML
private Button shamanButton;
@FXML
private Button mageButton;
@FXML
private Button priestButton;
@FXML
private Button collectionButton;
@FXML
private CheckBox arbitraryCheckBox;
private boolean arbitrary;
public ChooseClassView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/ChooseClassView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
arbitrary = false;
setupArbitraryBox();
warriorButton.setOnAction(this);
paladinButton.setOnAction(this);
druidButton.setOnAction(this);
rogueButton.setOnAction(this);
warlockButton.setOnAction(this);
hunterButton.setOnAction(this);
shamanButton.setOnAction(this);
mageButton.setOnAction(this);
priestButton.setOnAction(this);
collectionButton.setOnAction(this);
}
@Override
public void handle(ActionEvent event) {
Deck newDeck = null;
if (event.getSource() == warriorButton) {
newDeck = new Deck(HeroClass.WARRIOR, arbitrary);
} else if (event.getSource() == paladinButton) {
newDeck = new Deck(HeroClass.PALADIN, arbitrary);
} else if (event.getSource() == druidButton) {
newDeck = new Deck(HeroClass.DRUID, arbitrary);
} else if (event.getSource() == rogueButton) {
newDeck = new Deck(HeroClass.ROGUE, arbitrary);
} else if (event.getSource() == warlockButton) {
newDeck = new Deck(HeroClass.WARLOCK, arbitrary);
} else if (event.getSource() == hunterButton) {
newDeck = new Deck(HeroClass.HUNTER, arbitrary);
} else if (event.getSource() == shamanButton) {
newDeck = new Deck(HeroClass.SHAMAN, arbitrary);
} else if (event.getSource() == mageButton) {
newDeck = new Deck(HeroClass.MAGE, arbitrary);
} else if (event.getSource() == priestButton) {
newDeck = new Deck(HeroClass.PRIEST, arbitrary);
} else if (event.getSource() == collectionButton) {
newDeck = new MetaDeck();
}
NotificationProxy.sendNotification(GameNotification.SET_ACTIVE_DECK, newDeck);
}
private void onArbitraryBoxChanged(ObservableValue extends Boolean> ov, Boolean oldValue, Boolean newValue) {
arbitrary = newValue;
// deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
// deckProxy.setActiveDeckValidator(new ArbitraryDeckValidator());
}
private void setupArbitraryBox() {
arbitraryCheckBox.selectedProperty().addListener(this::onArbitraryBoxChanged);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/DeckBuilderMediator.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.util.ArrayList;
import java.util.List;
import net.demilich.nittygrittymvc.Mediator;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.DeckFormat;
import net.demilich.metastone.game.decks.validation.DefaultDeckValidator;
import net.demilich.metastone.gui.dialog.DialogNotification;
import net.demilich.metastone.gui.dialog.DialogType;
public class DeckBuilderMediator extends Mediator {
public static final String NAME = "DeckBuilderMediator";
private final DeckBuilderView view;
public DeckBuilderMediator() {
super(NAME);
view = new DeckBuilderView();
}
@SuppressWarnings("unchecked")
@Override
public void handleNotification(final INotification notification) {
switch (notification.getId()) {
case CREATE_NEW_DECK:
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
deckProxy.setActiveDeckValidator(new DefaultDeckValidator());
view.createNewDeck();
break;
case EDIT_DECK:
view.editDeck((Deck) notification.getBody());
break;
case ACTIVE_DECK_CHANGED:
view.activeDeckChanged((Deck) notification.getBody());
break;
case FILTERED_CARDS:
view.filteredCards((List) notification.getBody());
break;
case DECKS_LOADED:
view.displayDecks((List) notification.getBody());
break;
case INVALID_DECK_NAME:
DialogNotification dialogNotification = new DialogNotification("Name your deck", "Please enter a valid name for this deck.",
DialogType.WARNING);
getFacade().notifyObservers(dialogNotification);
break;
case DECK_FORMATS_LOADED:
List deckFormats = (List) notification.getBody();
view.injectDeckFormats(deckFormats);
break;
case DUPLICATE_DECK_NAME:
getFacade().notifyObservers(new DialogNotification("Duplicate deck name",
"This deck name was already used for another deck. Please choose another name", DialogType.WARNING));
break;
default:
break;
}
}
@Override
public List listNotificationInterests() {
List notificationInterests = new ArrayList();
notificationInterests.add(GameNotification.CREATE_NEW_DECK);
notificationInterests.add(GameNotification.EDIT_DECK);
notificationInterests.add(GameNotification.FILTERED_CARDS);
notificationInterests.add(GameNotification.ACTIVE_DECK_CHANGED);
notificationInterests.add(GameNotification.DECKS_LOADED);
notificationInterests.add(GameNotification.DECK_FORMATS_LOADED);
notificationInterests.add(GameNotification.INVALID_DECK_NAME);
notificationInterests.add(GameNotification.DUPLICATE_DECK_NAME);
return notificationInterests;
}
@Override
public void onRegister() {
getFacade().sendNotification(GameNotification.SHOW_VIEW, view);
getFacade().sendNotification(GameNotification.LOAD_DECKS);
getFacade().sendNotification(GameNotification.LOAD_DECK_FORMATS);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/DeckBuilderView.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.DeckFormat;
import net.demilich.metastone.game.decks.MetaDeck;
import net.demilich.metastone.gui.deckbuilder.metadeck.MetaDeckListView;
import net.demilich.metastone.gui.deckbuilder.metadeck.MetaDeckView;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class DeckBuilderView extends BorderPane implements EventHandler {
@FXML
private ScrollPane scrollPane;
@FXML
private Pane lowerInfoArea;
@FXML
private Pane upperInfoArea;
@FXML
private TextField importField;
@FXML
private Button importButton;
@FXML
private Button backButton;
private final CardView cardView;
private final CardListView cardListView;
private final DeckInfoView deckInfoView;
private final DeckListView deckListView;
private final DeckNameView deckNameView;
private final MetaDeckView metaDeckView;
private final MetaDeckListView metaDeckListView;
private List deckFormats = new ArrayList();
public DeckBuilderView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/DeckBuilderView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
importButton.setOnAction(this);
backButton.setOnAction(this);
cardView = new CardView();
cardListView = new CardListView();
deckInfoView = new DeckInfoView();
deckListView = new DeckListView();
deckNameView = new DeckNameView();
metaDeckView = new MetaDeckView();
metaDeckListView = new MetaDeckListView();
showSidebar(deckListView);
}
public void activeDeckChanged(Deck activeDeck) {
if (activeDeck.isMetaDeck()) {
MetaDeck metaDeck = (MetaDeck) activeDeck;
metaDeckListView.displayDecks(metaDeck.getDecks());
metaDeckView.deckChanged(metaDeck);
} else {
activeDeck.getCards().sortByManaCost();
cardListView.displayDeck(activeDeck);
}
deckInfoView.updateDeck(activeDeck);
deckNameView.updateDeck(activeDeck);
}
public void createNewDeck() {
showMainArea(new ChooseClassView());
showSidebar(null);
}
public void displayDecks(List decks) {
deckListView.displayDecks(decks);
metaDeckView.displayDecks(decks);
}
public void editDeck(Deck deck) {
if (deck.isMetaDeck()) {
showMainArea(metaDeckView);
showSidebar(metaDeckListView);
} else {
showMainArea(cardView);
showSidebar(cardListView);
showBottomBar(new CardFilterView(deckFormats));
}
showLowerInfoArea(deckInfoView);
showUpperInfoArea(deckNameView);
}
public void filteredCards(List filteredCards) {
cardView.displayCards(filteredCards);
}
@Override
public void handle(ActionEvent event) {
if (event.getSource() == importButton) {
NotificationProxy.sendNotification(GameNotification.IMPORT_DECK_FROM_URL, importField.getText());
} else if (event.getSource() == backButton) {
NotificationProxy.sendNotification(GameNotification.MAIN_MENU);
}
}
public void injectDeckFormats(List deckFormats) {
this.deckFormats.addAll(deckFormats);
}
private void showBottomBar(Node content) {
BorderPane.setAlignment(content, Pos.CENTER);
setBottom(content);
}
private void showLowerInfoArea(Node content) {
lowerInfoArea.getChildren().clear();
lowerInfoArea.getChildren().add(content);
}
private void showMainArea(Node content) {
setCenter(content);
}
private void showSidebar(Node content) {
scrollPane.setVisible(content != null);
scrollPane.setContent(content);
}
private void showUpperInfoArea(Node content) {
upperInfoArea.getChildren().clear();
upperInfoArea.getChildren().add(content);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/DeckEntry.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.IOException;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import net.demilich.metastone.ApplicationFacade;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.gui.IconFactory;
import net.demilich.metastone.gui.dialog.DialogNotification;
import net.demilich.metastone.gui.dialog.DialogResult;
import net.demilich.metastone.gui.dialog.DialogType;
public class DeckEntry extends HBox {
@FXML
private Label deckNameLabel;
@FXML
private ImageView classIcon;
@FXML
private Button deleteDeckButton;
private Deck deck;
public DeckEntry() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/DeckEntry.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
deleteDeckButton.setOnAction(this::handleDeleteDeck);
}
public Deck getDeck() {
return deck;
}
public void setDeck(Deck deck) {
this.deck = deck;
deckNameLabel.setText(deck.getName());
classIcon.setImage(IconFactory.getClassIcon(deck.getHeroClass()));
}
private void handleDeleteDeck(ActionEvent event) {
DialogNotification dialogNotification = new DialogNotification("Delete deck",
"Do you really want to delete the deck '" + deck.getName() + "'? This cannot be undone.", DialogType.WARNING);
dialogNotification.setHandler(this::onDeleteDeckDialog);
ApplicationFacade.getInstance().notifyObservers(dialogNotification);
}
private void onDeleteDeckDialog(DialogResult result) {
if (result == DialogResult.OK) {
NotificationProxy.sendNotification(GameNotification.DELETE_DECK, deck);
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/DeckFormatProxy.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.demilich.metastone.utils.ResourceInputStream;
import net.demilich.metastone.utils.ResourceLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.cards.CardSet;
import net.demilich.metastone.game.decks.DeckFormat;
import net.demilich.nittygrittymvc.Proxy;
public class DeckFormatProxy extends Proxy {
private static Logger logger = LoggerFactory.getLogger(DeckFormatProxy.class);
public static final String NAME = "DeckFormatProxy";
private static final String DECK_FORMATS_FOLDER = "formats";
private final List deckFormats = new ArrayList();
public DeckFormatProxy() {
super(NAME);
}
public DeckFormat getDeckFormatByName(String deckName) {
for (DeckFormat deckFormat : deckFormats) {
if (deckFormat.getName().equals(deckName)) {
return deckFormat;
}
}
return null;
}
public List getDeckFormats() {
return deckFormats;
}
public void loadDeckFormats() throws IOException, URISyntaxException {
deckFormats.clear();
// load the deck formats from the resources in cards.jar file on the classpath
Collection inputStreams = ResourceLoader.loadJsonInputStreams(DECK_FORMATS_FOLDER, false);
Gson gson = new GsonBuilder().setPrettyPrinting().create();
loadDeckFormats(inputStreams, gson);
}
private void loadDeckFormats(Collection inputStreams, Gson gson) throws FileNotFoundException {
for (ResourceInputStream resourceInputStream : inputStreams) {
Reader reader = new InputStreamReader(resourceInputStream.inputStream);
HashMap map = gson.fromJson(reader, new TypeToken>() {}.getType());
if (!map.containsKey("sets")) {
logger.error("Deck {} does not specify a value for 'sets' and is therefore not valid", resourceInputStream.fileName);
continue;
}
String deckName = (String) map.get("name");
DeckFormat deckFormat = null;
// this one is a meta deck; we need to parse those after all other
// decks are done
deckFormat = parseStandardDeckFormat(map);
deckFormat.setName(deckName);
deckFormat.setFilename(resourceInputStream.fileName);
deckFormats.add(deckFormat);
}
}
private DeckFormat parseStandardDeckFormat(Map map) {
DeckFormat deckFormat = new DeckFormat();
@SuppressWarnings("unchecked")
List setIds = (List) map.get("sets");
for (String setId : setIds) {
for (CardSet set : CardSet.values()) {
if (set.toString().equalsIgnoreCase(setId)) {
deckFormat.addSet(set);
}
}
}
return deckFormat;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/DeckInfoView.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.IOException;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import net.demilich.metastone.ApplicationFacade;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.MetaDeck;
import net.demilich.metastone.game.logic.GameLogic;
import net.demilich.metastone.gui.dialog.DialogNotification;
import net.demilich.metastone.gui.dialog.DialogResult;
import net.demilich.metastone.gui.dialog.DialogType;
import net.demilich.metastone.gui.dialog.IDialogListener;
public class DeckInfoView extends HBox implements EventHandler, IDialogListener {
@FXML
private Button doneButton;
@FXML
private Label typeLabel;
@FXML
private Label countLabel;
private Deck activeDeck;
public DeckInfoView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/DeckInfoView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
doneButton.setOnAction(this);
}
@Override
public void handle(ActionEvent event) {
if (activeDeck.isMetaDeck() && !activeDeck.isComplete()) {
DialogNotification dialogNotification = new DialogNotification("Warning",
"Your deck collection is not complete yet. Each deck collection has to contain at least 2 (or more) decks. ",
DialogType.WARNING);
ApplicationFacade.getInstance().notifyObservers(dialogNotification);
} else if (!activeDeck.isMetaDeck() && !activeDeck.isComplete() && !activeDeck.isTooBig() && !activeDeck.isArbitrary()) {
DialogNotification dialogNotification = new DialogNotification("Add random cards",
"Your deck is not complete yet. If you proceed, all open slots will be filled with random cards.", DialogType.CONFIRM);
dialogNotification.setHandler(this);
ApplicationFacade.getInstance().notifyObservers(dialogNotification);
} else if (!activeDeck.isMetaDeck() && !activeDeck.isComplete() && activeDeck.isTooBig() && !activeDeck.isArbitrary()) {
DialogNotification dialogNotification = new DialogNotification("Remove random cards",
"Your deck has too many cards. If you proceed, some cards will be removed at random.", DialogType.CONFIRM);
dialogNotification.setHandler(this);
ApplicationFacade.getInstance().notifyObservers(dialogNotification);
} else {
NotificationProxy.sendNotification(GameNotification.SAVE_ACTIVE_DECK);
}
}
@Override
public void onDialogClosed(DialogResult result) {
if (result == DialogResult.OK) {
NotificationProxy.sendNotification(GameNotification.FILL_DECK_WITH_RANDOM_CARDS);
NotificationProxy.sendNotification(GameNotification.SAVE_ACTIVE_DECK);
}
}
public void updateDeck(Deck deck) {
this.activeDeck = deck;
if (deck.isMetaDeck()) {
MetaDeck metaDeck = (MetaDeck) deck;
typeLabel.setText("Decks");
countLabel.setText(metaDeck.getDecks().size() + "");
} else {
typeLabel.setText("Cards");
if (deck.isTooBig()) {
countLabel.setText(deck.getCards().getCount() + "!/" + GameLogic.DECK_SIZE);
countLabel.setTextFill(Color.RED);
} else {
countLabel.setText(deck.getCards().getCount() + "/" + GameLogic.DECK_SIZE);
countLabel.setTextFill(Color.BLACK);
}
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/DeckListView.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.IOException;
import java.util.List;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.decks.Deck;
public class DeckListView extends VBox implements EventHandler {
@FXML
private Button newDeckButton;
public DeckListView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/DeckListView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
newDeckButton.setOnAction(actionEvent -> NotificationProxy.sendNotification(GameNotification.CREATE_NEW_DECK));
setCache(true);
}
private void clearChildren() {
for (Node child : getChildren()) {
child.removeEventHandler(MouseEvent.MOUSE_CLICKED, this);
}
getChildren().clear();
}
public void displayDecks(List decks) {
clearChildren();
getChildren().add(newDeckButton);
for (Deck deck : decks) {
DeckEntry deckEntry = new DeckEntry();
deckEntry.setDeck(deck);
deckEntry.addEventHandler(MouseEvent.MOUSE_CLICKED, this);
getChildren().add(deckEntry);
}
}
@Override
public void handle(MouseEvent event) {
DeckEntry deckEntry = (DeckEntry) event.getSource();
NotificationProxy.sendNotification(GameNotification.SET_ACTIVE_DECK, deckEntry.getDeck());
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/DeckNameView.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.IOException;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.TextField;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.gui.IconFactory;
public class DeckNameView extends HBox implements ChangeListener {
@FXML
private ImageView classIcon;
@FXML
private TextField nameField;
public DeckNameView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/DeckNameView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
nameField.textProperty().addListener(this);
}
@Override
public void changed(ObservableValue extends String> observable, String oldValue, String newValue) {
NotificationProxy.sendNotification(GameNotification.CHANGE_DECK_NAME, newValue);
}
public void updateDeck(Deck deck) {
classIcon.setImage(IconFactory.getClassIcon(deck.getHeroClass()));
nameField.setText(deck.getName());
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/DeckProxy.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.demilich.metastone.utils.MetastoneProperties;
import net.demilich.metastone.utils.ResourceInputStream;
import net.demilich.metastone.utils.ResourceLoader;
import net.demilich.metastone.utils.UserHomeMetastone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.cards.CardCatalogue;
import net.demilich.metastone.game.cards.CardCollection;
import net.demilich.metastone.game.cards.CardSet;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.DeckFormat;
import net.demilich.metastone.game.decks.MetaDeck;
import net.demilich.metastone.game.entities.heroes.HeroClass;
import net.demilich.metastone.game.decks.validation.DefaultDeckValidator;
import net.demilich.metastone.game.decks.validation.IDeckValidator;
import net.demilich.nittygrittymvc.Proxy;
public class DeckProxy extends Proxy {
private static Logger logger = LoggerFactory.getLogger(DeckProxy.class);
public static final String NAME = "DeckProxy";
private static final String DECKS_FOLDER = "decks";
private static final String DECKS_FOLDER_PATH = UserHomeMetastone.getPath() + File.separator + DECKS_FOLDER;
private static final String DECKS_COPIED_PROPERTY = "decks.copied";
private final List decks = new ArrayList();
private IDeckValidator activeDeckValidator = new DefaultDeckValidator();
private Deck activeDeck;
public DeckProxy() {
super(NAME);
try {
// ensure user's personal deck dir exists
Files.createDirectories(Paths.get(DECKS_FOLDER_PATH));
// ensure decks have been copied to ~/metastone/decks
copyDecksFromResources();
} catch (IOException e) {
logger.error("Trouble creating " + Paths.get(DECKS_FOLDER_PATH));
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
public boolean addCardToDeck(Card card) {
boolean result = activeDeckValidator.canAddCardToDeck(card, activeDeck);
if (result) {
activeDeck.getCards().add(card);
}
return result;
}
public Deck getActiveDeck() {
return activeDeck;
}
public List getCards(HeroClass heroClass) {
DeckFormat deckFormat = new DeckFormat();
for (CardSet cardSet : CardSet.values()) {
deckFormat.addSet(cardSet);
}
CardCollection cardCollection;
if (activeDeck.isArbitrary()) {
cardCollection = CardCatalogue.query(deckFormat);
} else {
cardCollection = CardCatalogue.query(deckFormat, heroClass);
// add neutral cards
cardCollection.addAll(CardCatalogue.query(deckFormat, HeroClass.ANY));
}
cardCollection.sortByName();
cardCollection.sortByManaCost();
return cardCollection.toList();
}
public Deck getDeckByName(String deckName) {
for (Deck deck : decks) {
if (deck.getName().equals(deckName)) {
return deck;
}
}
return null;
}
public List getDecks() {
return decks;
}
public void deleteDeck(Deck deck) {
decks.remove(deck);
logger.debug("Trying to delete deck '{}' contained in file '{}'...", deck.getName(), deck.getFilename());
Path path = Paths.get(DECKS_FOLDER_PATH + File.separator + deck.getFilename());
try {
Files.delete(path);
} catch (NoSuchFileException x) {
logger.error("Could not delete deck '{}' as the filename '{}' does not exist", deck.getName(), path);
return;
} catch (IOException e) {
logger.error(e.getMessage());
logger.error("Could not delete file '{}'", path);
return;
}
logger.info("Deck '{}' contained in file '{}' has been successfully deleted", deck.getName(), path.getFileName().toString());
getFacade().sendNotification(GameNotification.DECKS_LOADED, decks);
}
public void loadDecks() throws IOException, URISyntaxException {
decks.clear();
// load decks from ~/metastone/decks on the filesystem
loadStandardDecks(ResourceLoader.loadJsonInputStreams(DECKS_FOLDER_PATH, true), new GsonBuilder().setPrettyPrinting().create());
loadMetaDecks(ResourceLoader.loadJsonInputStreams(DECKS_FOLDER_PATH, true), new GsonBuilder().setPrettyPrinting().create());
}
private void copyDecksFromResources() throws IOException, URISyntaxException {
// if we have not copied decks to the USER_HOME_METASTONE decks folder,
// then do so now
if (!MetastoneProperties.getBoolean(DECKS_COPIED_PROPERTY)) {
ResourceLoader.copyFromResources(DECKS_FOLDER, DECKS_FOLDER_PATH);
// set a property to indicate that we have copied decks
MetastoneProperties.setBoolean(DECKS_COPIED_PROPERTY, true);
}
}
private void loadMetaDecks(Collection inputStreams, Gson gson) throws IOException {
for (ResourceInputStream resourceInputStream : inputStreams) {
Reader reader = new InputStreamReader(resourceInputStream.inputStream);
HashMap map = gson.fromJson(reader, new TypeToken>() {
}.getType());
if (!map.containsKey("heroClass")) {
logger.error("Deck {} does not specify a value for 'heroClass' and is therefor not valid", resourceInputStream.fileName);
continue;
}
String deckName = (String) map.get("name");
Deck deck = null;
if (!map.containsKey("decks")) {
continue;
} else {
deck = parseMetaDeck(map);
}
deck.setName(deckName);
deck.setFilename(resourceInputStream.fileName);
decks.add(deck);
}
}
private void loadStandardDecks(Collection inputStreams, Gson gson) throws FileNotFoundException {
for (ResourceInputStream resourceInputStream : inputStreams) {
Reader reader = new InputStreamReader(resourceInputStream.inputStream);
HashMap map = gson.fromJson(reader, new TypeToken>() {
}.getType());
if (!map.containsKey("heroClass")) {
logger.error("Deck {} does not speficy a value for 'heroClass' and is therefor not valid", resourceInputStream.fileName);
continue;
}
HeroClass heroClass = HeroClass.valueOf((String) map.get("heroClass"));
String deckName = (String) map.get("name");
Deck deck = null;
// this one is a meta deck; we need to parse those after all other
// decks are done
if (map.containsKey("decks")) {
continue;
} else {
deck = parseStandardDeck(deckName, heroClass, map);
}
deck.setName(deckName);
deck.setFilename(resourceInputStream.fileName);
decks.add(deck);
}
}
public boolean nameAvailable(Deck deck) {
for (Deck existingDeck : decks) {
if (existingDeck != deck && existingDeck.getName().equals(deck.getName())) {
return false;
}
}
return true;
}
private Deck parseMetaDeck(Map map) {
@SuppressWarnings("unchecked")
List referencedDecks = (List) map.get("decks");
List decksInMetaDeck = new ArrayList<>();
for (String deckName : referencedDecks) {
Deck deck = getDeckByName(deckName);
if (deck == null) {
logger.error("Metadeck {} contains invalid reference to deck {}", map.get("name"), deckName);
continue;
}
decksInMetaDeck.add(deck);
}
return new MetaDeck(decksInMetaDeck);
}
private Deck parseStandardDeck(String deckName, HeroClass heroClass, Map map) {
boolean arbitrary = false;
if (map.containsKey("arbitrary")) {
arbitrary = (boolean) map.get("arbitrary");
}
Deck deck = new Deck(heroClass, arbitrary);
@SuppressWarnings("unchecked")
List cardIds = (List) map.get("cards");
for (String cardId : cardIds) {
Card card = CardCatalogue.getCardById(cardId);
if (card == null) {
logger.error("Deck {} contains invalid cardId '{}'", deckName, cardId);
continue;
}
deck.getCards().add(card);
}
return deck;
}
public void removeCardFromDeck(Card card) {
activeDeck.getCards().remove(card);
}
public void saveActiveDeck() {
decks.add(activeDeck);
saveToJson(activeDeck);
activeDeck = null;
}
private void saveToJson(Deck deck) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
HashMap saveData = new HashMap();
saveData.put("name", deck.getName());
saveData.put("description", deck.getDescription());
saveData.put("arbitrary", deck.isArbitrary());
saveData.put("heroClass", deck.getHeroClass());
if (deck.isMetaDeck()) {
MetaDeck metaDeck = (MetaDeck) deck;
List referencedDecks = new ArrayList<>();
for (Deck referencedDeck : metaDeck.getDecks()) {
referencedDecks.add(referencedDeck.getName());
}
saveData.put("decks", referencedDecks);
} else {
List cardIds = new ArrayList();
for (Card card : deck.getCards()) {
cardIds.add(card.getCardId());
}
saveData.put("cards", cardIds);
}
String jsonData = gson.toJson(saveData);
try {
String filename = deck.getName().toLowerCase();
filename = filename.replaceAll(" ", "_");
filename = filename.replaceAll("\\W+", "");
filename = DECKS_FOLDER_PATH + File.separator + filename + ".json";
Path path = Paths.get(filename);
Files.write(path, jsonData.getBytes());
deck.setFilename(path.getFileName().toString());
} catch (IOException e) {
e.printStackTrace();
}
}
public void setActiveDeck(Deck activeDeck) {
this.activeDeck = activeDeck;
}
public void setActiveDeckValidator(IDeckValidator deckValidator) {
this.activeDeckValidator = deckValidator;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/DeleteDeckCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
public class DeleteDeckCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
Deck deck = (Deck) notification.getBody();
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
deckProxy.deleteDeck(deck);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/FillDeckWithRandomCardsCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.decks.Deck;
public class FillDeckWithRandomCardsCommand extends SimpleCommand {
private static Logger logger = LoggerFactory.getLogger(FillDeckWithRandomCardsCommand.class);
@Override
public void execute(INotification notification) {
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
Deck activeDeck = deckProxy.getActiveDeck();
List cards = deckProxy.getCards(activeDeck.getHeroClass());
if (activeDeck.isTooBig()) {
while (!activeDeck.isComplete()) {
Card randomCard = activeDeck.getCards().getRandom();
deckProxy.removeCardFromDeck(randomCard);
logger.debug("Removing card {} to deck.", randomCard);
}
} else {
while (!activeDeck.isComplete()) {
Card randomCard = cards.get(ThreadLocalRandom.current().nextInt(cards.size()));
if (deckProxy.addCardToDeck(randomCard)) {
logger.debug("Adding card {} to deck.", randomCard);
}
}
}
getFacade().sendNotification(GameNotification.ACTIVE_DECK_CHANGED, activeDeck);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/FilterCardsCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.cards.CardSet;
import net.demilich.metastone.game.decks.DeckFormat;
public class FilterCardsCommand extends SimpleCommand {
private static List filterByFormat(List collection, DeckFormat format) {
if (format == null) {
return collection;
}
collection.removeIf(card -> !format.isInFormat(card));
return collection;
}
private static List filterBySet(List collection, CardSet set) {
if (set == CardSet.ANY) {
return collection;
}
collection.removeIf(card -> card.getCardSet() != set);
return collection;
}
private static List filterByText(List collection, String text) {
if (StringUtils.isBlank(text)) {
return collection;
}
String filterText = text.toLowerCase();
collection.removeIf(card -> !card.matchesFilter(filterText));
return collection;
}
@Override
public void execute(INotification notification) {
CardFilter filter = (CardFilter) notification.getBody();
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
List cards = deckProxy.getCards(deckProxy.getActiveDeck().getHeroClass());
cards = filterByFormat(cards, filter.getFormat());
cards = filterBySet(cards, filter.getSet());
cards = filterByText(cards, filter.getText());
getFacade().sendNotification(GameNotification.FILTERED_CARDS, cards);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/ImportDeckCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import net.demilich.metastone.gui.deckbuilder.importer.ImporterFactory;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.gui.deckbuilder.importer.IDeckImporter;
import net.demilich.metastone.gui.dialog.DialogNotification;
import net.demilich.metastone.gui.dialog.DialogType;
public class ImportDeckCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
String url = (String) notification.getBody();
ImporterFactory factory = new ImporterFactory();
IDeckImporter importer = factory.createDeckImporter(url);
Deck importedDeck = null;
if(importer != null)
importedDeck = importer.importFrom(url);
if (importedDeck == null) {
DialogNotification dialogNotification = new DialogNotification("Error",
"Import of deck failed. Please make sure to provide a valid URL. At the moment, only hearthpwn.com, tempostorm.com, icy-veins.com, and heartheed.com are supported for deck import.",
DialogType.ERROR);
notifyObservers(dialogNotification);
return;
}
getFacade().sendNotification(GameNotification.SET_ACTIVE_DECK, importedDeck);
getFacade().sendNotification(GameNotification.SAVE_ACTIVE_DECK);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/LoadDeckFormatsCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URISyntaxException;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
public class LoadDeckFormatsCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
DeckFormatProxy deckFormatProxy = (DeckFormatProxy) getFacade().retrieveProxy(DeckFormatProxy.NAME);
try {
deckFormatProxy.loadDeckFormats();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
getFacade().sendNotification(GameNotification.DECK_FORMATS_LOADED, deckFormatProxy.getDeckFormats());
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/LoadDecksCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URISyntaxException;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
public class LoadDecksCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
try {
deckProxy.loadDecks();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
getFacade().sendNotification(GameNotification.DECKS_LOADED, deckProxy.getDecks());
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/RemoveCardFromDeckCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.cards.Card;
public class RemoveCardFromDeckCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
Card card = (Card) notification.getBody();
deckProxy.getActiveDeck().getCards().remove(card);
getFacade().sendNotification(GameNotification.ACTIVE_DECK_CHANGED, deckProxy.getActiveDeck());
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/SaveDeckCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
public class SaveDeckCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
String deckName = deckProxy.getActiveDeck().getName().trim();
if (deckName == null || deckName.equals("")) {
getFacade().sendNotification(GameNotification.INVALID_DECK_NAME);
return;
} else if (!deckProxy.nameAvailable(deckProxy.getActiveDeck())) {
getFacade().sendNotification(GameNotification.DUPLICATE_DECK_NAME);
return;
}
deckProxy.saveActiveDeck();
getFacade().removeMediator(DeckBuilderMediator.NAME);
getFacade().sendNotification(GameNotification.MAIN_MENU);
getFacade().sendNotification(GameNotification.DECK_BUILDER_SELECTED);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/SetActiveDeckCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.validation.ArbitraryDeckValidator;
import net.demilich.metastone.game.decks.validation.DefaultDeckValidator;
public class SetActiveDeckCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
Deck activeDeck = (Deck) notification.getBody();
if (activeDeck.isArbitrary()) {
deckProxy.setActiveDeckValidator(new ArbitraryDeckValidator());
} else {
deckProxy.setActiveDeckValidator(new DefaultDeckValidator());
}
deckProxy.setActiveDeck(activeDeck);
getFacade().sendNotification(GameNotification.EDIT_DECK, activeDeck);
getFacade().sendNotification(GameNotification.FILTERED_CARDS, deckProxy.getCards(activeDeck.getHeroClass()));
getFacade().sendNotification(GameNotification.ACTIVE_DECK_CHANGED, activeDeck);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/importer/HearthHeadImporter.java
================================================
package net.demilich.metastone.gui.deckbuilder.importer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.jsoup.Connection.Response;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.cards.CardCatalogue;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.entities.heroes.HeroClass;
public class HearthHeadImporter implements IDeckImporter {
private static Logger logger = LoggerFactory.getLogger(IcyVeinsImporter.class);
@Override
public Deck importFrom(String url) {
String exportUrl = url;
try {
return parse(exportUrl);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private List getCardIds(Document doc){
Elements metas = doc.getElementsByTag("meta");
String title = "";
String cards = "";
for (int i = 0; i < metas.size(); i++) {
if (metas.get(i).attr("property").equals("x-hearthstone:deck:cards")) {
cards = metas.get(i).attr("content");
}
if (metas.get(i).attr("property").equals("x-hearthstone:deck")) {
title = metas.get(i).attr("content");
}
}
List cs = new ArrayList();
cs.add(0, title);
List cids = Arrays.asList(cards.split(","));
for (String c: cids){
cs.add(c);
}
return cs;
}
private Deck parse(String url) throws IOException {
List cards = new ArrayList();
HeroClass heroClass = HeroClass.ANY;
Response response= Jsoup.connect(url)
.ignoreContentType(true)
.userAgent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0")
.referrer("http://www.google.com")
.timeout(12000)
.followRedirects(true)
.execute();
Document doc = response.parse();
for (String cid: getCardIds(doc).subList(1, getCardIds(doc).size())) {
Card card = CardCatalogue.getCardByBlizzardId(cid);
if (card != null) {
cards.add(card);
if (card.getHeroClass() != HeroClass.ANY) {
heroClass = card.getHeroClass();
}
} else {
logger.error("Card with id {} could not be found", cid);
}
}
Deck deck = new Deck(heroClass);
deck.setName(getCardIds(doc).get(0));
for (Card card : cards) {
deck.getCards().add(card);
}
if (!deck.isComplete()) {
return null;
}
return deck;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/importer/HearthPwnImporter.java
================================================
package net.demilich.metastone.gui.deckbuilder.importer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.cards.CardCatalogue;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.entities.heroes.HeroClass;
public class HearthPwnImporter implements IDeckImporter {
private static Logger logger = LoggerFactory.getLogger(HearthPwnImporter.class);
private String extractId(String url) {
String result = "";
boolean digitEncountered = false;
for (int i = 0; i < url.length(); i++) {
char c = url.charAt(i);
if (Character.isDigit(c)) {
result += c;
digitEncountered = true;
} else if (digitEncountered) {
break;
}
}
return result;
}
private String getExportUrl(String url) {
String idString = extractId(url);
String result = "http://www.hearthpwn.com/decks/{}/export/2".replace("{}", idString);
return result;
}
@Override
public Deck importFrom(String url) {
try {
RequestConfig globalConfig = RequestConfig.custom().setCircularRedirectsAllowed(true).build();
CloseableHttpClient httpclient = HttpClientBuilder.create().build();
String exportUrl = getExportUrl(url);
logger.debug("Requesting: " + exportUrl);
HttpGet httpGet = new HttpGet(exportUrl);
httpGet.setConfig(globalConfig);
CloseableHttpResponse response = httpclient.execute(httpGet);
try {
HttpEntity entity = response.getEntity();
String htmlContent = EntityUtils.toString(entity);
EntityUtils.consume(entity);
return parse(htmlContent);
} finally {
response.close();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private Deck parse(String htmlContent) {
List cards = new ArrayList();
HeroClass heroClass = HeroClass.ANY;
// remove html tags
htmlContent = htmlContent.replaceAll("\\<.+?\\>", "");
// remove BBCode tags
htmlContent = htmlContent.replaceAll("\\[.+?\\]", "");
// remove empty lines
htmlContent = htmlContent.replaceAll("(?m)^\\s+", "");
// unescape
htmlContent = StringEscapeUtils.unescapeHtml4(htmlContent);
String lines[] = htmlContent.split("\\r?\\n");
String deckName = lines[0];
for (String line : lines) {
if (!line.startsWith("1") && !line.startsWith("2")) {
continue;
}
int count = Integer.parseInt(String.valueOf(line.charAt(0)));
String cardName = line.substring(4);
for (int i = 0; i < count; i++) {
Card card = CardCatalogue.getCardByName(cardName);
if (card != null) {
cards.add(card);
if (card.getHeroClass() != HeroClass.ANY) {
heroClass = card.getHeroClass();
}
} else {
logger.error("Card with name {} could not be found", cardName);
}
}
}
Deck deck = new Deck(heroClass);
deck.setName(deckName);
for (Card card : cards) {
deck.getCards().add(card);
}
if (!deck.isComplete()) {
return null;
}
return deck;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/importer/IDeckImporter.java
================================================
package net.demilich.metastone.gui.deckbuilder.importer;
import net.demilich.metastone.game.decks.Deck;
public interface IDeckImporter {
Deck importFrom(String uri);
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/importer/IcyVeinsImporter.java
================================================
package net.demilich.metastone.gui.deckbuilder.importer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.cards.CardCatalogue;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.entities.heroes.HeroClass;
public class IcyVeinsImporter implements IDeckImporter {
private static Logger logger = LoggerFactory.getLogger(IcyVeinsImporter.class);
@Override
public Deck importFrom(String url) {
try {
RequestConfig globalConfig = RequestConfig.custom().setCircularRedirectsAllowed(true).build();
CloseableHttpClient httpclient = HttpClientBuilder.create().build();
String exportUrl = url;
logger.debug("Requesting: " + exportUrl);
HttpGet httpGet = new HttpGet(exportUrl);
httpGet.setConfig(globalConfig);
CloseableHttpResponse response = httpclient.execute(httpGet);
try {
HttpEntity entity = response.getEntity();
String htmlContent = EntityUtils.toString(entity);
EntityUtils.consume(entity);
return parse(htmlContent);
} finally {
response.close();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private Deck parse(String htmlContent) {
List cards = new ArrayList();
HeroClass heroClass = HeroClass.ANY;
Document doc = Jsoup.parse(htmlContent);
String deckName = doc.getElementsByClass("page_breadcrumbs_item").last().text();
Elements cardLines = doc.getElementsByClass("deck_card_list").get(0).getElementsByTag("li");
for (Element e: cardLines){
if (!e.text().startsWith("1") && !e.text().startsWith("2")) {
continue;
}
int count = Integer.parseInt(String.valueOf(e.text().charAt(0)));
String cardName = e.getElementsByTag("a").get(0).text();
for (int i = 0; i < count; i++){
Card card = CardCatalogue.getCardByName(cardName);
if (card != null) {
cards.add(card);
if (card.getHeroClass() != HeroClass.ANY) {
heroClass = card.getHeroClass();
}
} else {
logger.error("Card with name {} could not be found", cardName);
}
}
}
Deck deck = new Deck(heroClass);
deck.setName(deckName);
for (Card card : cards) {
deck.getCards().add(card);
logger.debug("Card added - {}", card.getName());
}
if (!deck.isComplete()) {
logger.error("Deck with name only has {}.", deck.getCards().toList().size());
return null;
}
return deck;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/importer/ImporterFactory.java
================================================
package net.demilich.metastone.gui.deckbuilder.importer;
public class ImporterFactory {
public IDeckImporter createDeckImporter(String url)
{
if(url == null)
return null;
if(url.contains("hearthpwn.com"))
return new HearthPwnImporter();
if(url.contains("tempostorm.com"))
return new TempostormImporter();
if(url.contains("icy-veins.com"))
return new IcyVeinsImporter();
if(url.contains("hearthhead.com"))
return new HearthHeadImporter();
return null;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/importer/TempostormImporter.java
================================================
package net.demilich.metastone.gui.deckbuilder.importer;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.cards.CardCatalogue;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.entities.heroes.HeroClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TempostormImporter implements IDeckImporter{
private static Logger logger = LoggerFactory.getLogger(TempostormImporter.class);
Deck parse(JsonObject root)
{
try {
List cards = new ArrayList();
String deckName = root.get("name").getAsString();
String hero = root.get("playerClass").getAsString();
HeroClass heroClass = HeroClass.valueOf(hero.toUpperCase());
JsonElement cardsEl = root.get("cards");
JsonArray cardsArray = cardsEl.getAsJsonArray();
for (JsonElement cardTypeElem : cardsArray) {
JsonObject cardTypeObj = cardTypeElem.getAsJsonObject();
int cardCount = cardTypeObj.get("cardQuantity").getAsInt();
JsonObject cardObj = cardTypeObj.get("card").getAsJsonObject();
String cardName = cardObj.get("name").getAsString();
Card card = CardCatalogue.getCardByName(cardName);
if (card != null) {
if(cardCount > 0)
cards.add(card);
for (int i = 1; i < cardCount; i++)
cards.add(card.clone());
} else {
logger.error("Card with name {} could not be found", cardName);
return null;
}
}
Deck deck = new Deck(heroClass);
deck.setName(deckName);
for (Card card : cards)
deck.getCards().add(card);
if (!deck.isComplete())
return null;
return deck;
}
catch(Exception e)
{
e.printStackTrace();
return null;
}
}
String convertUrl(String url)
{
Pattern pattern = Pattern.compile(".*/decks/([^/]+)$");
Matcher matcher = pattern.matcher(url);
if(!matcher.matches())
return null;
String identifier = matcher.group(1);
String filter = "{\"where\":{\"slug\":\"" + identifier
+ "\"},\"fields\":{},\"include\":[{\"relation\":\"cards\",\"scope\":{\"include\":[\"card\"]}}]}";
return "https://tempostorm.com/api/decks/findOne?filter=" + filter;
}
@Override
public Deck importFrom(String requestedUrl) {
String apiUrl = convertUrl(requestedUrl);
logger.debug("Requesting: " + apiUrl);
URL url;
try {
url = new URL(apiUrl);
} catch (MalformedURLException e) {
e.printStackTrace();
return null;
}
HttpURLConnection request;
try {
request = (HttpURLConnection) url.openConnection();
request.connect();
JsonParser jp = new JsonParser();
JsonElement root = jp.parse(new InputStreamReader((InputStream) request.getContent()));
JsonObject jobj = root.getAsJsonObject();
return parse(jobj);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/metadeck/AddDeckToMetaDeckCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder.metadeck;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.MetaDeck;
import net.demilich.metastone.gui.deckbuilder.DeckProxy;
public class AddDeckToMetaDeckCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
MetaDeck metaDeck = (MetaDeck) deckProxy.getActiveDeck();
Deck deck = (Deck) notification.getBody();
if (metaDeck.getDecks().contains(deck)) {
return;
}
metaDeck.getDecks().add(deck);
getFacade().sendNotification(GameNotification.ACTIVE_DECK_CHANGED, deckProxy.getActiveDeck());
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/metadeck/MetaDeckListView.java
================================================
package net.demilich.metastone.gui.deckbuilder.metadeck;
import java.io.IOException;
import java.util.List;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.gui.deckbuilder.DeckEntry;
public class MetaDeckListView extends VBox implements EventHandler {
@FXML
private Button newDeckButton;
public MetaDeckListView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/MetaDeckListView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
setCache(true);
}
public void displayDecks(List decks) {
getChildren().clear();
for (Deck deck : decks) {
DeckEntry deckEntry = new DeckEntry();
deckEntry.setDeck(deck);
deckEntry.addEventHandler(MouseEvent.MOUSE_CLICKED, this);
getChildren().add(deckEntry);
}
}
@Override
public void handle(MouseEvent event) {
DeckEntry deckEntry = (DeckEntry) event.getSource();
NotificationProxy.sendNotification(GameNotification.REMOVE_DECK_FROM_META_DECK, deckEntry.getDeck());
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/metadeck/MetaDeckView.java
================================================
package net.demilich.metastone.gui.deckbuilder.metadeck;
import java.io.IOException;
import java.util.List;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.MetaDeck;
import net.demilich.metastone.gui.IconFactory;
public class MetaDeckView extends BorderPane {
@FXML
private Pane contentPane;
public MetaDeckView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/MetaDeckView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
setCache(true);
}
public void deckChanged(MetaDeck metaDeck) {
for (Node node : contentPane.getChildren()) {
Deck deck = (Deck) node.getUserData();
node.setDisable(metaDeck.getDecks().contains(deck));
}
}
public void displayDecks(List decks) {
contentPane.getChildren().clear();
for (Deck deck : decks) {
if (deck.isMetaDeck()) {
continue;
}
ImageView graphic = new ImageView(IconFactory.getClassIcon(deck.getHeroClass()));
graphic.setFitWidth(48);
graphic.setFitHeight(48);
Button deckButton = new Button(deck.getName(), graphic);
deckButton.setMaxWidth(160);
deckButton.setMinWidth(160);
deckButton.setMaxHeight(120);
deckButton.setMinHeight(120);
deckButton.setWrapText(true);
deckButton.setContentDisplay(ContentDisplay.LEFT);
deckButton.setOnAction(event -> NotificationProxy.sendNotification(GameNotification.ADD_DECK_TO_META_DECK, deck));
deckButton.setUserData(deck);
contentPane.getChildren().add(deckButton);
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/deckbuilder/metadeck/RemoveDeckFromMetaDeckCommand.java
================================================
package net.demilich.metastone.gui.deckbuilder.metadeck;
import net.demilich.nittygrittymvc.SimpleCommand;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.MetaDeck;
import net.demilich.metastone.gui.deckbuilder.DeckProxy;
public class RemoveDeckFromMetaDeckCommand extends SimpleCommand {
@Override
public void execute(INotification notification) {
DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME);
MetaDeck metaDeck = (MetaDeck) deckProxy.getActiveDeck();
Deck deck = (Deck) notification.getBody();
if (!metaDeck.getDecks().contains(deck)) {
return;
}
metaDeck.getDecks().remove(deck);
getFacade().sendNotification(GameNotification.ACTIVE_DECK_CHANGED, deckProxy.getActiveDeck());
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/dialog/DialogMediator.java
================================================
package net.demilich.metastone.gui.dialog;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.demilich.nittygrittymvc.Mediator;
import net.demilich.nittygrittymvc.interfaces.INotification;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.Pane;
import javafx.stage.Window;
import net.demilich.metastone.GameNotification;
public class DialogMediator extends Mediator {
public static final String NAME = "DialogMediator";
private static Logger logger = LoggerFactory.getLogger(DialogMediator.class);
private Window root;
public DialogMediator() {
super(NAME);
}
@Override
public void handleNotification(final INotification notification) {
switch (notification.getId()) {
case CANVAS_CREATED:
Pane canvas = (Pane) notification.getBody();
root = canvas.getScene().getWindow();
break;
case SHOW_USER_DIALOG:
showUserDialog((DialogNotification) notification);
break;
case SHOW_MODAL_DIALOG:
showModalDialog((Node) notification.getBody());
break;
case CARD_PARSE_ERROR:
displayErrorMessage("Something is wrong with your card files", (String) notification.getBody());
break;
default:
logger.warn("Unhandled notification {} in {}", notification, getClass().getSimpleName());
break;
}
}
@Override
public List listNotificationInterests() {
List notificationInterests = new ArrayList();
notificationInterests.add(GameNotification.CANVAS_CREATED);
notificationInterests.add(GameNotification.SHOW_MODAL_DIALOG);
notificationInterests.add(GameNotification.SHOW_USER_DIALOG);
notificationInterests.add(GameNotification.CARD_PARSE_ERROR);
return notificationInterests;
}
private void displayErrorMessage(String header, String message) {
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText(header);
alert.setContentText(message);
alert.showAndWait();
}
private void showModalDialog(Node content) {
new ModalDialog(root, content);
}
private void showUserDialog(DialogNotification notification) {
UserDialog userDialog = new UserDialog(notification.getTitle(), notification.getMessage(), notification.getDialogType());
userDialog.setDialogHandler(notification.getHandler());
showModalDialog(userDialog);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/dialog/DialogNotification.java
================================================
package net.demilich.metastone.gui.dialog;
import net.demilich.nittygrittymvc.Notification;
import net.demilich.metastone.GameNotification;
public class DialogNotification extends Notification {
private final String title;
private final String message;
private final DialogType dialogType;
private IDialogListener handler;
public DialogNotification(String title, String message, DialogType dialogType) {
super(GameNotification.SHOW_USER_DIALOG);
this.title = title;
this.message = message;
this.dialogType = dialogType;
}
public DialogType getDialogType() {
return dialogType;
}
public IDialogListener getHandler() {
return handler;
}
public String getMessage() {
return message;
}
public String getTitle() {
return title;
}
public void setHandler(IDialogListener handler) {
this.handler = handler;
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/dialog/DialogResult.java
================================================
package net.demilich.metastone.gui.dialog;
public enum DialogResult {
OK, CANCEL
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/dialog/DialogType.java
================================================
package net.demilich.metastone.gui.dialog;
public enum DialogType {
CONFIRM, INFO, WARNING, ERROR,
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/dialog/IDialogListener.java
================================================
package net.demilich.metastone.gui.dialog;
public interface IDialogListener {
void onDialogClosed(DialogResult result);
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/dialog/ModalDialog.java
================================================
package net.demilich.metastone.gui.dialog;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
public class ModalDialog extends StackPane {
public ModalDialog(Window parent, Node content) {
Stage stage = new Stage();
Scene scene = new Scene(this);
scene.setFill(null);
stage.setScene(scene);
stage.initModality(Modality.WINDOW_MODAL);
stage.initStyle(StageStyle.TRANSPARENT);
stage.initOwner(parent);
stage.setX(parent.getX());
stage.setY(parent.getY());
setPrefSize(parent.getWidth(), parent.getHeight());
setStyle("-fx-background-color: rgba(0, 0, 0, 0.5);");
getChildren().add(content);
stage.show();
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/dialog/UserDialog.java
================================================
package net.demilich.metastone.gui.dialog;
import java.io.IOException;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import net.demilich.metastone.gui.IconFactory;
public class UserDialog extends BorderPane implements EventHandler {
@FXML
private Label headerLabel;
@FXML
private Label textLabel;
@FXML
private ImageView icon;
@FXML
private Button positiveButton;
@FXML
private Button negativeButton;
private IDialogListener dialogHandler;
public UserDialog(String title, String message, DialogType dialogType) {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/UserDialog.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
icon.setImage(IconFactory.getDialogIcon(dialogType));
headerLabel.setText(title);
textLabel.setText(message);
positiveButton.setOnAction(this);
negativeButton.setOnAction(this);
}
@Override
public void handle(ActionEvent event) {
if (event.getSource() == positiveButton) {
setDialogResult(DialogResult.OK);
} else if (event.getSource() == negativeButton) {
setDialogResult(DialogResult.CANCEL);
}
}
public void setDialogHandler(IDialogListener dialogHandler) {
this.dialogHandler = dialogHandler;
}
private void setDialogResult(DialogResult result) {
if (dialogHandler != null) {
dialogHandler.onDialogClosed(result);
}
this.getScene().getWindow().hide();
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/gameconfig/PlayerConfigView.java
================================================
package net.demilich.metastone.gui.gameconfig;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import net.demilich.metastone.game.behaviour.GreedyOptimizeMove;
import net.demilich.metastone.game.behaviour.IBehaviour;
import net.demilich.metastone.game.behaviour.NoAggressionBehaviour;
import net.demilich.metastone.game.behaviour.PlayRandomBehaviour;
import net.demilich.metastone.game.behaviour.heuristic.WeightedHeuristic;
import net.demilich.metastone.game.behaviour.human.HumanBehaviour;
import net.demilich.metastone.game.behaviour.threat.GameStateValueBehaviour;
import net.demilich.metastone.game.behaviour.FlatMonteCarlo;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.cards.CardCatalogue;
import net.demilich.metastone.game.cards.HeroCard;
import net.demilich.metastone.game.decks.Deck;
import net.demilich.metastone.game.decks.DeckFactory;
import net.demilich.metastone.game.decks.DeckFormat;
import net.demilich.metastone.game.entities.heroes.HeroClass;
import net.demilich.metastone.game.entities.heroes.MetaHero;
import net.demilich.metastone.game.gameconfig.PlayerConfig;
import net.demilich.metastone.gui.IconFactory;
import net.demilich.metastone.gui.common.BehaviourStringConverter;
import net.demilich.metastone.gui.common.DeckStringConverter;
import net.demilich.metastone.gui.common.HeroStringConverter;
import net.demilich.metastone.gui.playmode.config.PlayerConfigType;
public class PlayerConfigView extends VBox {
@FXML
protected Label heroNameLabel;
@FXML
protected ImageView heroIcon;
@FXML
protected ComboBox behaviourBox;
@FXML
protected ComboBox heroBox;
@FXML
protected ComboBox deckBox;
@FXML
protected CheckBox hideCardsCheckBox;
private final PlayerConfig playerConfig = new PlayerConfig();
private List decks = new ArrayList();
private PlayerConfigType selectionHint;
private DeckFormat deckFormat;
public PlayerConfigView(PlayerConfigType selectionHint) {
this.selectionHint = selectionHint;
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/PlayerConfigView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
heroBox.setConverter(new HeroStringConverter());
deckBox.setConverter(new DeckStringConverter());
behaviourBox.setConverter(new BehaviourStringConverter());
setupHideCardsBox(selectionHint);
setupHeroes();
setupBehaviours();
deckBox.valueProperty().addListener((ChangeListener) (observableProperty, oldDeck, newDeck) -> {
getPlayerConfig().setDeck(newDeck);
});
}
private void filterDecks() {
HeroClass heroClass = getPlayerConfig().getHeroCard().getHeroClass();
ObservableList deckList = FXCollections.observableArrayList();
if (heroClass == HeroClass.DECK_COLLECTION) {
for (Deck deck : decks) {
if (deck.getHeroClass() != HeroClass.DECK_COLLECTION) {
continue;
}
if (deckFormat != null && deckFormat.isInFormat(deck)) {
deckList.add(deck);
}
}
} else {
Deck randomDeck = DeckFactory.getRandomDeck(heroClass, deckFormat);
deckList.add(randomDeck);
for (Deck deck : decks) {
if (deck.getHeroClass() == HeroClass.DECK_COLLECTION) {
continue;
}
if (deck.getHeroClass() == heroClass || deck.getHeroClass() == HeroClass.ANY) {
if (deckFormat != null && deckFormat.isInFormat(deck)) {
deckList.add(deck);
}
}
}
}
deckBox.setItems(deckList);
deckBox.getSelectionModel().selectFirst();
}
public PlayerConfig getPlayerConfig() {
return playerConfig;
}
public void injectDecks(List decks) {
this.decks = decks;
heroBox.getSelectionModel().selectFirst();
behaviourBox.getSelectionModel().selectFirst();
}
private void onBehaviourChanged(ObservableValue extends IBehaviour> ov, IBehaviour oldBehaviour, IBehaviour newBehaviour) {
getPlayerConfig().setBehaviour(newBehaviour);
boolean humanBehaviourSelected = newBehaviour instanceof HumanBehaviour;
hideCardsCheckBox.setDisable(humanBehaviourSelected);
if (humanBehaviourSelected) {
hideCardsCheckBox.setSelected(false);
}
}
private void onHideCardBoxChanged(ObservableValue extends Boolean> ov, Boolean oldValue, Boolean newValue) {
playerConfig.setHideCards(newValue);
}
private void selectHero(HeroCard heroCard) {
Image heroPortrait = new Image(IconFactory.getHeroIconUrl(heroCard.getHeroClass()));
heroIcon.setImage(heroPortrait);
heroNameLabel.setText(heroCard.getName());
getPlayerConfig().setHeroCard(heroCard);
filterDecks();
}
public void setupBehaviours() {
ObservableList behaviourList = FXCollections.observableArrayList();
if (selectionHint == PlayerConfigType.HUMAN || selectionHint == PlayerConfigType.SANDBOX) {
behaviourList.add(new HumanBehaviour());
}
behaviourList.add(new GameStateValueBehaviour());
if (selectionHint == PlayerConfigType.OPPONENT) {
behaviourList.add(new HumanBehaviour());
}
behaviourList.add(new PlayRandomBehaviour());
behaviourList.add(new GreedyOptimizeMove(new WeightedHeuristic()));
behaviourList.add(new NoAggressionBehaviour());
behaviourList.add(new FlatMonteCarlo(100));
behaviourBox.setItems(behaviourList);
behaviourBox.valueProperty().addListener(this::onBehaviourChanged);
}
public void setupHeroes() {
ObservableList heroList = FXCollections.observableArrayList();
for (Card card : CardCatalogue.getHeroes()) {
heroList.add((HeroCard) card);
}
heroList.add(new MetaHero());
heroBox.setItems(heroList);
heroBox.valueProperty().addListener((ChangeListener) (observableValue, oldHero, newHero) -> {
selectHero(newHero);
});
}
private void setupHideCardsBox(PlayerConfigType configType) {
hideCardsCheckBox.selectedProperty().addListener(this::onHideCardBoxChanged);
hideCardsCheckBox.setSelected(selectionHint == PlayerConfigType.OPPONENT);
if (configType == PlayerConfigType.SIMULATION || configType == PlayerConfigType.SANDBOX) {
hideCardsCheckBox.setVisible(false);
}
}
public void setDeckFormat(DeckFormat newDeckFormat) {
deckFormat = newDeckFormat;
filterDecks();
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/main/ApplicationMediator.java
================================================
package net.demilich.metastone.gui.main;
import java.util.ArrayList;
import java.util.List;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.gui.battleofdecks.BattleOfDecksMediator;
import net.demilich.metastone.gui.deckbuilder.DeckBuilderMediator;
import net.demilich.metastone.gui.mainmenu.MainMenuMediator;
import net.demilich.metastone.gui.playmode.PlayModeMediator;
import net.demilich.metastone.gui.playmode.config.PlayModeConfigMediator;
import net.demilich.metastone.gui.sandboxmode.SandboxModeMediator;
import net.demilich.metastone.gui.simulationmode.SimulationMediator;
import net.demilich.metastone.gui.trainingmode.TrainingModeMediator;
import net.demilich.nittygrittymvc.Mediator;
import net.demilich.nittygrittymvc.interfaces.INotification;
public class ApplicationMediator extends Mediator {
public static final String NAME = "ApplicationMediator";
private Pane root;
public ApplicationMediator() {
super(NAME);
}
@Override
public void handleNotification(final INotification notification) {
switch (notification.getId()) {
case CANVAS_CREATED:
root = (Pane) notification.getBody();
break;
case SHOW_VIEW:
final Node view = (Node) notification.getBody();
root.getChildren().clear();
root.getChildren().add(view);
break;
case MAIN_MENU:
removeOtherViews();
getFacade().registerMediator(new MainMenuMediator());
break;
default:
break;
}
}
@Override
public List listNotificationInterests() {
List notificationInterests = new ArrayList();
notificationInterests.add(GameNotification.CANVAS_CREATED);
notificationInterests.add(GameNotification.SHOW_VIEW);
notificationInterests.add(GameNotification.MAIN_MENU);
notificationInterests.add(GameNotification.CARD_PARSE_ERROR);
return notificationInterests;
}
private void removeOtherViews() {
getFacade().removeMediator(PlayModeMediator.NAME);
getFacade().removeMediator(PlayModeConfigMediator.NAME);
getFacade().removeMediator(DeckBuilderMediator.NAME);
getFacade().removeMediator(SimulationMediator.NAME);
getFacade().removeMediator(TrainingModeMediator.NAME);
getFacade().removeMediator(SandboxModeMediator.NAME);
getFacade().removeMediator(BattleOfDecksMediator.NAME);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/mainmenu/MainMenuMediator.java
================================================
package net.demilich.metastone.gui.mainmenu;
import java.util.ArrayList;
import java.util.List;
import net.demilich.nittygrittymvc.Mediator;
import net.demilich.nittygrittymvc.interfaces.INotification;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.gui.battleofdecks.BattleOfDecksMediator;
import net.demilich.metastone.gui.deckbuilder.DeckBuilderMediator;
import net.demilich.metastone.gui.playmode.config.PlayModeConfigMediator;
import net.demilich.metastone.gui.sandboxmode.SandboxModeMediator;
import net.demilich.metastone.gui.simulationmode.SimulationMediator;
import net.demilich.metastone.gui.trainingmode.TrainingModeMediator;
public class MainMenuMediator extends Mediator {
public static final String NAME = "MainMenuMediator";
private final MainMenuView view;
public MainMenuMediator() {
super(NAME);
view = new MainMenuView();
}
@Override
public void handleNotification(final INotification notification) {
switch (notification.getId()) {
case DECK_BUILDER_SELECTED:
getFacade().registerMediator(new DeckBuilderMediator());
break;
case PLAY_MODE_SELECTED:
getFacade().registerMediator(new PlayModeConfigMediator());
break;
case SIMULATION_MODE_SELECTED:
getFacade().registerMediator(new SimulationMediator());
break;
case SANDBOX_MODE_SELECTED:
getFacade().registerMediator(new SandboxModeMediator());
break;
case TRAINING_MODE_SELECTED:
getFacade().registerMediator(new TrainingModeMediator());
break;
case BATTLE_OF_DECKS_SELECTED:
getFacade().registerMediator(new BattleOfDecksMediator());
break;
default:
break;
}
getFacade().removeMediator(MainMenuMediator.NAME);
}
@Override
public List listNotificationInterests() {
List notificationInterests = new ArrayList();
notificationInterests.add(GameNotification.DECK_BUILDER_SELECTED);
notificationInterests.add(GameNotification.PLAY_MODE_SELECTED);
notificationInterests.add(GameNotification.SIMULATION_MODE_SELECTED);
notificationInterests.add(GameNotification.SANDBOX_MODE_SELECTED);
notificationInterests.add(GameNotification.TRAINING_MODE_SELECTED);
notificationInterests.add(GameNotification.BATTLE_OF_DECKS_SELECTED);
return notificationInterests;
}
@Override
public void onRegister() {
getFacade().sendNotification(GameNotification.SHOW_VIEW, view);
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/mainmenu/MainMenuView.java
================================================
package net.demilich.metastone.gui.mainmenu;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import net.demilich.metastone.BuildConfig;
import net.demilich.metastone.GameNotification;
import net.demilich.metastone.NotificationProxy;
public class MainMenuView extends BorderPane {
@FXML
private Button deckBuilderButton;
@FXML
private Button playModeButton;
@FXML
private Button simulationModeButton;
@FXML
private Button sandboxModeButton;
@FXML
private Button trainingModeButton;
@FXML
private Button battleOfDecksButton;
@FXML
private Label versionLabel;
@FXML
private Button donationButton;
public MainMenuView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/MainMenuView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
deckBuilderButton.setOnAction(event -> NotificationProxy.sendNotification(GameNotification.DECK_BUILDER_SELECTED));
playModeButton.setOnAction(event -> NotificationProxy.sendNotification(GameNotification.PLAY_MODE_SELECTED));
simulationModeButton
.setOnAction(event -> NotificationProxy.sendNotification(GameNotification.SIMULATION_MODE_SELECTED));
sandboxModeButton.setOnAction(event -> NotificationProxy.sendNotification(GameNotification.SANDBOX_MODE_SELECTED));
trainingModeButton.setOnAction(event -> NotificationProxy.sendNotification(GameNotification.TRAINING_MODE_SELECTED));
battleOfDecksButton
.setOnAction(event -> NotificationProxy.sendNotification(GameNotification.BATTLE_OF_DECKS_SELECTED));
if (!BuildConfig.DEV_BUILD) {
trainingModeButton.setVisible(false);
trainingModeButton.setManaged(false);
battleOfDecksButton.setVisible(false);
battleOfDecksButton.setManaged(false);
}
versionLabel.setText(BuildConfig.VERSION + (BuildConfig.DEV_BUILD ? " (Dev build)" : ""));
donationButton.setOnAction(this::openDonation);
}
private void openDonation(ActionEvent event) {
try {
java.awt.Desktop.getDesktop()
.browse(new URI("https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=92DYWPZUVDMEY"));
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
}
================================================
FILE: app/src/main/java/net/demilich/metastone/gui/playmode/GameBoardView.java
================================================
package net.demilich.metastone.gui.playmode;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import net.demilich.metastone.game.GameContext;
import net.demilich.metastone.game.Player;
import net.demilich.metastone.game.actions.ActionType;
import net.demilich.metastone.game.actions.GameAction;
import net.demilich.metastone.game.behaviour.human.HumanTargetOptions;
import net.demilich.metastone.game.cards.CardCollection;
import net.demilich.metastone.game.entities.Actor;
import net.demilich.metastone.game.entities.Entity;
import net.demilich.metastone.game.entities.minions.Summon;
import net.demilich.metastone.game.logic.GameLogic;
import net.demilich.metastone.gui.IconFactory;
import net.demilich.metastone.gui.cards.HandCard;
import net.demilich.metastone.gui.playmode.animation.EventVisualizerDispatcher;
public class GameBoardView extends BorderPane {
@FXML
private HBox p1CardPane;
@FXML
private HBox p2CardPane;
@FXML
private HBox p1MinionPane;
@FXML
private HBox p2MinionPane;
@FXML
private VBox p1HeroAnchor;
@FXML
private VBox p2HeroAnchor;
@FXML
private HBox centerMessageArea;
private HeroToken p1Hero;
private HeroToken p2Hero;
private HandCard[] p1Cards = new HandCard[GameLogic.MAX_HAND_CARDS];
private HandCard[] p2Cards = new HandCard[GameLogic.MAX_HAND_CARDS];
private SummonToken[] p1Minions = new SummonToken[GameLogic.MAX_MINIONS];
private SummonToken[] p2Minions = new SummonToken[GameLogic.MAX_MINIONS];
private final HashMap summonHelperMap1 = new HashMap();
private final HashMap summonHelperMap2 = new HashMap();
private final HashMap entityTokenMap = new HashMap();
private final EventVisualizerDispatcher gameEventVisualizer = new EventVisualizerDispatcher();
@FXML
private Label centerMessageLabel;
public GameBoardView() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/GameBoardView.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
// initialize card ui elements
for (int i = 0; i < p1Cards.length; i++) {
p1Cards[i] = new HandCard();
p1Cards[i].setVisible(false);
p2Cards[i] = new HandCard();
p2Cards[i].setVisible(false);
}
p1CardPane.getChildren().addAll(p1Cards);
p2CardPane.getChildren().addAll(p2Cards);
// initialize minion tokens elements
for (int i = 0; i < p1Minions.length; i++) {
Button summonHelper = createSummonHelper();
p1MinionPane.getChildren().add(summonHelper);
p1Minions[i] = new SummonToken();
p1MinionPane.getChildren().add(p1Minions[i]);
summonHelperMap1.put(p1Minions[i], summonHelper);
summonHelper = createSummonHelper();
p2MinionPane.getChildren().add(summonHelper);
p2Minions[i] = new SummonToken();
p2MinionPane.getChildren().add(p2Minions[i]);
summonHelperMap2.put(p2Minions[i], summonHelper);
}
// create one additional summon helper (for each player)
Button summonHelper = createSummonHelper();
p1MinionPane.getChildren().add(summonHelper);
summonHelperMap1.put(null, summonHelper);
summonHelper = createSummonHelper();
p2MinionPane.getChildren().add(summonHelper);
summonHelperMap2.put(null, summonHelper);
p1Hero = new HeroToken();
p2Hero = new HeroToken();
p1HeroAnchor.getChildren().add(p1Hero);
p2HeroAnchor.getChildren().add(p2Hero);
}
private void checkForWinner(GameContext context) {
if (context.gameDecided()) {
if (context.getWinningPlayerId() == -1) {
centerMessageLabel.setStyle("-fx-text-fill: red;");
setCenterMessage("Game has ended in a draw.");
} else {
centerMessageLabel.setStyle("-fx-text-fill: green;");
Player winner = context.getPlayer(context.getWinningPlayerId());
setCenterMessage("Player " + winner.getName() + " has won the game.");
}
}
}
private Button createSummonHelper() {
ImageView icon = new ImageView(IconFactory.getSummonHelper());
icon.setFitWidth(32);
icon.setFitHeight(32);
Button helper = new Button("", icon);
helper.setStyle("-fx-padding: 2 2 2 2;");
helper.setVisible(false);
helper.setManaged(false);
return helper;
}
public void disableTargetSelection() {
for (GameToken token : entityTokenMap.values()) {
token.hideTargetMarker();
}
for (Button summonHelper : summonHelperMap1.values()) {
summonHelper.setVisible(false);
summonHelper.setManaged(false);
}
for (Button summonHelper : summonHelperMap2.values()) {
summonHelper.setVisible(false);
summonHelper.setManaged(false);
}
hideCenterMessage();
}
private void enableSpellTargets(final HumanTargetOptions targetOptions) {
GameContext context = targetOptions.getContext();
for (final GameAction action : targetOptions.getActionGroup().getActionsInGroup()) {
Entity target = context.resolveSingleTarget(action.getTargetKey());
GameToken token = getToken(target);
EventHandler clickedHander = new EventHandler() {
@Override
public void handle(MouseEvent event) {
disableTargetSelection();
targetOptions.getActionSelectionListener().onActionSelected(action);
}
};
token.showTargetMarker(clickedHander);
}
}
private void enableSummonTargets(final HumanTargetOptions targetOptions) {
int playerId = targetOptions.getPlayerId();
GameContext context = targetOptions.getContext();
for (final GameAction action : targetOptions.getActionGroup().getActionsInGroup()) {
Entity target = context.resolveSingleTarget(action.getTargetKey());
GameToken token = getToken(target);
Button summonHelper = playerId == 0 ? summonHelperMap1.get(token) : summonHelperMap2.get(token);
summonHelper.setVisible(true);
summonHelper.setManaged(true);
EventHandler clickedHander = new EventHandler