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 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 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 observable, CardSet oldValue, CardSet newValue) { filterChanged(); } private void textChanged(ObservableValue 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 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 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 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 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() { @Override public void handle(ActionEvent event) { disableTargetSelection(); targetOptions.getActionSelectionListener().onActionSelected(action); } }; summonHelper.setOnAction(clickedHander); } } public void enableTargetSelection(final HumanTargetOptions targetOptions) { GameAction action = targetOptions.getActionGroup().getPrototype(); if (action.getActionType() == ActionType.SUMMON) { enableSummonTargets(targetOptions); } else { enableSpellTargets(targetOptions); } setCenterMessage("Select target for " + action.getPromptText() + " - ESC to cancel"); } public GameToken getToken(Entity entity) { return entityTokenMap.get(entity); } private void hideCenterMessage() { centerMessageLabel.setVisible(false); } private void setCenterMessage(String message) { centerMessageLabel.setText(message); centerMessageLabel.setVisible(true); } public void showAnimations(GameContext context) { gameEventVisualizer.visualize((GameContextVisualizable) context, this); } public void updateGameState(GameContext context) { entityTokenMap.clear(); p1Hero.setHero(context.getPlayer1()); p1Hero.updateHeroPowerCost(context, context.getPlayer1()); p1Hero.highlight(context.getActivePlayer() == context.getPlayer1()); entityTokenMap.put(context.getPlayer1().getHero(), p1Hero); p2Hero.setHero(context.getPlayer2()); p2Hero.updateHeroPowerCost(context, context.getPlayer2()); p2Hero.highlight(context.getActivePlayer() == context.getPlayer2()); entityTokenMap.put(context.getPlayer2().getHero(), p2Hero); updateHandCards(context, context.getPlayer1(), p1Cards); updateHandCards(context, context.getPlayer2(), p2Cards); updateSummonTokens(context.getPlayer1(), p1Minions); updateSummonTokens(context.getPlayer2(), p2Minions); checkForWinner(context); } private void updateHandCards(GameContext context, Player player, HandCard[] handCards) { CardCollection hand = player.getHand(); for (int i = 0; i < handCards.length; i++) { if (i < hand.getCount()) { handCards[i].setManaged(true); handCards[i].setVisible(true); handCards[i].setCard(context, hand.get(i), player); } else { handCards[i].setManaged(false); handCards[i].setVisible(false); } } } private void updateSummonTokens(Player player, SummonToken[] summonTokens) { List summons = player.getSummons(); for (int i = 0; i < summonTokens.length; i++) { if (i < summons.size()) { Summon summon = summons.get(i); summonTokens[i].setSummon(summon); summonTokens[i].setManaged(true); summonTokens[i].setVisible(true); entityTokenMap.put(summon, summonTokens[i]); } else { summonTokens[i].setManaged(false); summonTokens[i].setVisible(false); } } } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/GameContextVisualizable.java ================================================ package net.demilich.metastone.gui.playmode; import java.util.ArrayList; import java.util.List; import net.demilich.metastone.BuildConfig; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.Player; import net.demilich.metastone.game.actions.GameAction; import net.demilich.metastone.game.decks.DeckFormat; import net.demilich.metastone.game.events.GameEvent; import net.demilich.metastone.game.logic.GameLogic; public class GameContextVisualizable extends GameContext { private final List gameEvents = new ArrayList<>(); private boolean blockedByAnimation; public GameContextVisualizable(Player player1, Player player2, GameLogic logic, DeckFormat deckFormat) { super(player1, player2, logic, deckFormat); } protected boolean acceptAction(GameAction nextAction) { if (!ignoreEvents()) { return true; } while (ignoreEvents()) { try { Thread.sleep(BuildConfig.DEFAULT_SLEEP_DELAY); } catch (InterruptedException e) { } } return false; } @Override public void fireGameEvent(GameEvent gameEvent) { if (ignoreEvents()) { return; } super.fireGameEvent(gameEvent); getGameEvents().add(gameEvent); } public synchronized List getGameEvents() { return gameEvents; } public boolean isBlockedByAnimation() { return blockedByAnimation; } @Override protected void onGameStateChanged() { if (ignoreEvents()) { return; } setBlockedByAnimation(true); NotificationProxy.sendNotification(GameNotification.GAME_STATE_UPDATE, this); while (blockedByAnimation) { try { Thread.sleep(BuildConfig.DEFAULT_SLEEP_DELAY); } catch (InterruptedException e) { } } NotificationProxy.sendNotification(GameNotification.GAME_STATE_LATE_UPDATE, this); } public void setBlockedByAnimation(boolean blockedByAnimation) { this.blockedByAnimation = blockedByAnimation; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/GameToken.java ================================================ package net.demilich.metastone.gui.playmode; import java.io.IOException; import javafx.beans.binding.Bindings; import javafx.event.EventHandler; import javafx.fxml.FXMLLoader; import javafx.scene.Group; 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.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import net.demilich.metastone.gui.DigitFactory; import net.demilich.metastone.gui.IconFactory; public class GameToken extends BorderPane { protected StackPane target; private ImageView targetButton; private EventHandler existingEventHandler; public GameToken(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); } createTargetButton(); } private void createTargetButton() { target = (StackPane) lookup("#targetAnchor"); Image image = IconFactory.getTargetIcon(); ImageView targetIcon = new ImageView(image); targetIcon.setClip(new ImageView(image)); ColorAdjust monochrome = new ColorAdjust(); monochrome.setSaturation(-1.0); Blend red = new Blend(BlendMode.MULTIPLY, monochrome, new ColorInput(0, 0, targetIcon.getImage().getWidth(), targetIcon.getImage().getHeight(), Color.RED)); Blend green = new Blend(BlendMode.MULTIPLY, monochrome, new ColorInput(0, 0, targetIcon.getImage().getWidth(), targetIcon.getImage().getHeight(), new Color(0, 1, 0, 0.5))); targetButton = targetIcon; targetIcon.effectProperty().bind(Bindings.when(targetButton.hoverProperty()).then((Effect) green).otherwise((Effect) red)); targetButton.setId("target_button"); hideTargetMarker(); target.getChildren().add(targetButton); } public StackPane getAnchor() { return target; } public void hideTargetMarker() { targetButton.setVisible(false); } 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); } protected 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); } public void showTargetMarker(EventHandler clickedHander) { if (existingEventHandler != null) { targetButton.removeEventHandler(MouseEvent.MOUSE_CLICKED, existingEventHandler); } targetButton.addEventHandler(MouseEvent.MOUSE_CLICKED, clickedHander); targetButton.setVisible(true); existingEventHandler = clickedHander; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/HeroToken.java ================================================ package net.demilich.metastone.gui.playmode; import java.util.HashSet; import javafx.fxml.FXML; import javafx.scene.Group; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.Pane; import javafx.scene.shape.Shape; 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.cards.CardCatalogue; import net.demilich.metastone.game.cards.QuestCard; import net.demilich.metastone.game.entities.heroes.Hero; import net.demilich.metastone.game.entities.weapons.Weapon; import net.demilich.metastone.gui.IconFactory; import net.demilich.metastone.gui.cards.CardTooltip; public class HeroToken extends GameToken { @FXML private Group attackAnchor; @FXML private Group hpAnchor; @FXML private Label cardsLabel; @FXML private Label manaLabel; @FXML private Group armorAnchor; @FXML private ImageView armorIcon; @FXML private Pane weaponPane; @FXML private Label weaponNameLabel; @FXML private Group weaponAttackAnchor; @FXML private Group weaponDurabilityAnchor; @FXML private ImageView portrait; @FXML private Group heroPowerAnchor; @FXML private ImageView heroPowerIcon; @FXML private Pane secretsAnchor; @FXML private Shape frozen; public HeroToken() { super("HeroToken.fxml"); frozen.getStrokeDashArray().add(16.0); } public void highlight(boolean highlight) { String cssBorder = null; if (highlight) { cssBorder = "-fx-border-color:seagreen; \n" + "-fx-border-radius:7;\n" + "-fx-border-width:5.0;"; } else { cssBorder = "-fx-border-color:transparent; \n" + "-fx-border-radius:7;\n" + "-fx-border-width:5.0;"; } target.setStyle(cssBorder); } public void setHero(Player player) { Hero hero = player.getHero(); setScoreValue(attackAnchor, hero.getAttack()); Image portraitImage = new Image(IconFactory.getHeroIconUrl(hero.getHeroClass())); portrait.setImage(portraitImage); setScoreValue(hpAnchor, hero.getHp(), hero.getAttributeValue(Attribute.BASE_HP), hero.getMaxHp()); if (!player.getDeck().isEmpty()) { cardsLabel.setText("Cards in deck: " + player.getDeck().getCount()); } else { cardsLabel.setText("Fatigue: " + player.getAttributeValue(Attribute.FATIGUE)); } if (player.getAttributeValue(Attribute.OVERLOAD) > 0) { manaLabel.setText("Mana: " + player.getMana() + "/" + player.getMaxMana() + "\nOver: " + player.getAttributeValue(Attribute.OVERLOAD)); } else { manaLabel.setText("Mana: " + player.getMana() + "/" + player.getMaxMana()); } updateArmor(hero.getArmor()); updateHeroPower(hero); updateWeapon(hero.getWeapon()); updateSecrets(player); updateStatus(hero); } private void updateArmor(int armor) { setScoreValue(armorAnchor, armor); boolean visible = armor > 0; armorIcon.setVisible(visible); armorAnchor.setVisible(visible); } private void updateHeroPower(Hero hero) { Image heroPowerImage = new Image(IconFactory.getHeroPowerIconUrl(hero.getHeroPower())); heroPowerIcon.setImage(heroPowerImage); Card card = CardCatalogue.getCardById(hero.getHeroPower().getCardId()); Tooltip tooltip = new Tooltip(); CardTooltip tooltipContent = new CardTooltip(); tooltipContent.setCard(card); tooltip.setGraphic(tooltipContent); Tooltip.install(heroPowerIcon, tooltip); } public void updateHeroPowerCost(GameContext context, Player player) { setScoreValueLowerIsBetter(heroPowerAnchor, context.getLogic().getModifiedManaCost(player, player.getHero().getHeroPower()), player.getHero().getHeroPower().getBaseManaCost()); } private void updateSecrets(Player player) { secretsAnchor.getChildren().clear(); HashSet secretsCopy = new HashSet(player.getSecrets()); for (String secretId : secretsCopy) { Card card = CardCatalogue.getCardById(secretId); ImageView secretIcon = null; if (card instanceof QuestCard) { secretIcon = new ImageView(IconFactory.getImageUrl("common/quest.png")); } else { secretIcon = new ImageView(IconFactory.getImageUrl("common/secret.png")); } secretsAnchor.getChildren().add(secretIcon); if (!player.hideCards() || card instanceof QuestCard) { Tooltip tooltip = new Tooltip(); CardTooltip tooltipContent = new CardTooltip(); tooltipContent.setCard(card); tooltip.setGraphic(tooltipContent); Tooltip.install(secretIcon, tooltip); } } } private void updateStatus(Hero hero) { frozen.setVisible(hero.hasAttribute(Attribute.FROZEN)); } private void updateWeapon(Weapon weapon) { boolean hasWeapon = weapon != null; weaponPane.setVisible(hasWeapon); if (hasWeapon) { weaponNameLabel.setText(weapon.getName()); setScoreValue(weaponAttackAnchor, weapon.getWeaponDamage(), weapon.getBaseAttack()); setScoreValue(weaponDurabilityAnchor, weapon.getDurability(), weapon.getBaseDurability(), weapon.getMaxDurability()); Tooltip tooltip = new Tooltip(); CardTooltip tooltipContent = new CardTooltip(); tooltipContent.setCard(weapon.getSourceCard()); tooltip.setGraphic(tooltipContent); Tooltip.install(weaponPane, tooltip); } } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/HumanActionPromptView.java ================================================ package net.demilich.metastone.gui.playmode; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.text.TextAlignment; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.actions.ActionType; import net.demilich.metastone.game.actions.BattlecryAction; import net.demilich.metastone.game.actions.DiscoverAction; import net.demilich.metastone.game.actions.GameAction; import net.demilich.metastone.game.actions.HeroPowerAction; import net.demilich.metastone.game.actions.PhysicalAttackAction; import net.demilich.metastone.game.actions.PlayCardAction; import net.demilich.metastone.game.behaviour.human.ActionGroup; import net.demilich.metastone.game.behaviour.human.HumanActionOptions; import net.demilich.metastone.game.behaviour.human.HumanTargetOptions; import net.demilich.metastone.game.cards.Card; import net.demilich.metastone.game.entities.Entity; import net.demilich.metastone.game.targeting.TargetSelection; import net.demilich.metastone.gui.cards.CardTooltip; public class HumanActionPromptView extends VBox { private static String getActionString(GameContext context, GameAction action) { PlayCardAction playCardAction = null; Card card = null; String actionString = ""; switch (action.getActionType()) { case HERO_POWER: HeroPowerAction heroPowerAction = (HeroPowerAction) action; card = context.resolveCardReference(heroPowerAction.getCardReference()); actionString = "HERO POWER: " + card.getName(); break; case BATTLECRY: BattlecryAction battlecry = (BattlecryAction) action; actionString = "BATTLECRY " + battlecry.getSpell().getSpellClass().getSimpleName(); break; case PHYSICAL_ATTACK: PhysicalAttackAction physicalAttackAction = (PhysicalAttackAction) action; Entity attacker = context.resolveSingleTarget(physicalAttackAction.getAttackerReference()); actionString = "ATTACK with " + attacker.getName(); break; case SPELL: playCardAction = (PlayCardAction) action; card = context.resolveCardReference(playCardAction.getCardReference()); actionString = "CAST SPELL: " + card.getName(); break; case SUMMON: playCardAction = (PlayCardAction) action; card = context.resolveCardReference(playCardAction.getCardReference()); actionString = "SUMMON: " + card.getName(); break; case EQUIP_WEAPON: playCardAction = (PlayCardAction) action; card = context.resolveCardReference(playCardAction.getCardReference()); actionString = "WEAPON: " + card.getName(); break; case END_TURN: actionString = "END TURN"; break; case DISCOVER: DiscoverAction discover = (DiscoverAction) action; actionString = "DISCOVER " + discover.getSpell().getSpellClass().getSimpleName(); break; default: return ""; } if (action.getActionSuffix() != null) { actionString += " (" + action.getActionSuffix() + ")"; } return actionString; } private final List existingButtons = new ArrayList(); public HumanActionPromptView() { Label headerLabel = new Label("Choose action:"); headerLabel.setStyle("-fx-font-family: Arial;-fx-font-weight: bold; -fx-font-size: 16pt;"); setPrefWidth(Region.USE_COMPUTED_SIZE); setSpacing(2); setPadding(new Insets(8)); setAlignment(Pos.CENTER); getChildren().add(headerLabel); } private Node createActionButton(final ActionGroup actionGroup, HumanActionOptions options) { GameContext context = options.getContext(); Button button = new Button(getActionString(context, actionGroup.getPrototype())); button.setStyle("-fx-font-size: 12px; -fx-padding: 4 8 4 8;"); button.setWrapText(true); button.setPrefWidth(200); button.setTextAlignment(TextAlignment.CENTER); switch (actionGroup.getPrototype().getActionType()) { case BATTLECRY: break; case DISCOVER: CardTooltip tooltipContent = new CardTooltip(); DiscoverAction discover = (DiscoverAction) actionGroup.getPrototype(); Card card = discover.getCard(); if (card != null) { tooltipContent.setCard(card); Tooltip tooltip = new Tooltip(); tooltip.setGraphic(tooltipContent); Tooltip.install(button, tooltip); } else { tooltipContent.setNonCard(discover.getName(), discover.getDescription()); Tooltip tooltip = new Tooltip(); tooltip.setGraphic(tooltipContent); Tooltip.install(button, tooltip); } break; case END_TURN: break; case EQUIP_WEAPON: break; case HERO_POWER: break; case PHYSICAL_ATTACK: break; case SPELL: break; case SUMMON: break; case SYSTEM: break; default: break; } // only one action with no target selection or summon with no other // minion on board if (actionGroup.getActionsInGroup().size() == 1 && (actionGroup.getPrototype().getTargetRequirement() == TargetSelection.NONE || actionGroup.getPrototype().getActionType() == ActionType.SUMMON)) { button.setOnAction(event -> { options.getBehaviour().onActionSelected(actionGroup.getPrototype()); setVisible(false); }); return button; } HumanTargetOptions humanTargetOptions = new HumanTargetOptions(options.getBehaviour(), context, options.getPlayer().getId(), actionGroup); button.setOnAction(event -> { NotificationProxy.sendNotification(GameNotification.HUMAN_PROMPT_FOR_TARGET, humanTargetOptions); setVisible(false); }); return button; } private Collection createActionButtons(HumanActionOptions options) { Collection buttons = new ArrayList(); Collection actionGrups = groupActions(options); for (ActionGroup actionGroup : actionGrups) { buttons.add(createActionButton(actionGroup, options)); } return buttons; } private Collection groupActions(HumanActionOptions options) { Collection actionGroups = new ArrayList<>(); for (GameAction action : options.getValidActions()) { if (!matchesExistingGroup(action, actionGroups)) { ActionGroup newActionGroup = new ActionGroup(action); actionGroups.add(newActionGroup); } } return actionGroups; } private boolean matchesExistingGroup(GameAction action, Collection existingActionGroups) { for (ActionGroup actionGroup : existingActionGroups) { if (actionGroup.getPrototype().isSameActionGroup(action)) { actionGroup.add(action); return true; } } return false; } public void setActions(HumanActionOptions options) { getChildren().removeAll(existingButtons); existingButtons.clear(); Collection buttons = createActionButtons(options); existingButtons.addAll(buttons); getChildren().addAll(buttons); setVisible(true); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/HumanMulliganView.java ================================================ package net.demilich.metastone.gui.playmode; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Button; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.behaviour.human.HumanMulliganOptions; import net.demilich.metastone.game.cards.Card; import net.demilich.metastone.gui.IconFactory; import net.demilich.metastone.gui.cards.CardTooltip; public class HumanMulliganView extends BorderPane implements EventHandler { private class MulliganEntry { public boolean mulligan; public ImageView discardIcon; public MulliganEntry(ImageView icon) { this.discardIcon = icon; } } @FXML private HBox contentArea; @FXML private Button doneButton; private final HashMap mulliganState = new HashMap(); public HumanMulliganView(HumanMulliganOptions options) { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/HumanMulliganView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } displayCards(options); NotificationProxy.sendNotification(GameNotification.SHOW_MODAL_DIALOG, this); } private void displayCards(final HumanMulliganOptions options) { contentArea.getChildren().clear(); for (Card card : options.getOfferedCards()) { StackPane stackPane = new StackPane(); CardTooltip cardWidget = new CardTooltip(); cardWidget.setCard(card); cardWidget.addEventHandler(MouseEvent.MOUSE_CLICKED, this); stackPane.getChildren().add(cardWidget); ImageView mulliganIcon = new ImageView(IconFactory.getImageUrl("common/mulligan.png")); mulliganIcon.setMouseTransparent(true); mulliganIcon.setVisible(false); stackPane.getChildren().add(mulliganIcon); contentArea.getChildren().add(stackPane); mulliganState.put(card, new MulliganEntry(mulliganIcon)); } doneButton.setOnAction(event -> { List discardedCards = new ArrayList<>(); for (Card card : mulliganState.keySet()) { MulliganEntry entry = mulliganState.get(card); if (entry.mulligan) { discardedCards.add(card); } } options.getBehaviour().setMulliganCards(discardedCards); this.getScene().getWindow().hide(); }); } @Override public void handle(MouseEvent mouseEvent) { CardTooltip cardWidget = (CardTooltip) mouseEvent.getSource(); Card card = cardWidget.getCard(); MulliganEntry entry = mulliganState.get(card); entry.mulligan = !entry.mulligan; entry.discardIcon.setVisible(entry.mulligan); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/LoadingBoardView.java ================================================ package net.demilich.metastone.gui.playmode; import java.io.IOException; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.ProgressIndicator; import javafx.scene.layout.BorderPane; public class LoadingBoardView extends BorderPane { @FXML private ProgressIndicator loadingIndicator; public LoadingBoardView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/LoadingBoardView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } loadingIndicator.setProgress(-1); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/PlayModeMediator.java ================================================ package net.demilich.metastone.gui.playmode; import java.util.ArrayList; import java.util.List; import net.demilich.nittygrittymvc.Mediator; import net.demilich.nittygrittymvc.interfaces.INotification; import javafx.application.Platform; import javafx.event.EventHandler; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import net.demilich.metastone.GameNotification; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.behaviour.human.HumanActionOptions; import net.demilich.metastone.game.behaviour.human.HumanMulliganOptions; import net.demilich.metastone.game.behaviour.human.HumanTargetOptions; public class PlayModeMediator extends Mediatorimplements EventHandler { public static final String NAME = "PlayModeMediator"; private final PlayModeView view; private final HumanActionPromptView actionPromptView; public PlayModeMediator() { super(NAME); view = new PlayModeView(); actionPromptView = view.getActionPromptView(); } @Override public void handle(KeyEvent keyEvent) { if (keyEvent.getCode() != KeyCode.ESCAPE) { return; } view.disableTargetSelection(); } @Override public void handleNotification(final INotification notification) { switch (notification.getId()) { case GAME_STATE_UPDATE: GameContext context = (GameContext) notification.getBody(); Platform.runLater(() -> view.showAnimations(context)); break; case GAME_STATE_LATE_UPDATE: GameContext context2 = (GameContext) notification.getBody(); Platform.runLater(() -> view.updateGameState(context2)); break; case HUMAN_PROMPT_FOR_ACTION: HumanActionOptions actionOptions = (HumanActionOptions) notification.getBody(); Platform.runLater(() -> actionPromptView.setActions(actionOptions)); break; case HUMAN_PROMPT_FOR_TARGET: HumanTargetOptions options = (HumanTargetOptions) notification.getBody(); Platform.runLater(() -> view.enableTargetSelection(options)); break; case HUMAN_PROMPT_FOR_MULLIGAN: HumanMulliganOptions mulliganOptions = (HumanMulliganOptions) notification.getBody(); Platform.runLater(() -> new HumanMulliganView(mulliganOptions)); break; default: break; } } @Override public List listNotificationInterests() { List notificationInterests = new ArrayList(); notificationInterests.add(GameNotification.GAME_STATE_UPDATE); notificationInterests.add(GameNotification.GAME_STATE_LATE_UPDATE); notificationInterests.add(GameNotification.HUMAN_PROMPT_FOR_ACTION); notificationInterests.add(GameNotification.HUMAN_PROMPT_FOR_TARGET); notificationInterests.add(GameNotification.HUMAN_PROMPT_FOR_MULLIGAN); notificationInterests.add(GameNotification.REPLY_DECKS); notificationInterests.add(GameNotification.REPLY_DECK_FORMATS); return notificationInterests; } @Override public void onRegister() { getFacade().sendNotification(GameNotification.SHOW_VIEW, view); view.getScene().setOnKeyPressed(this); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/PlayModeView.java ================================================ package net.demilich.metastone.gui.playmode; import java.io.IOException; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Button; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.behaviour.human.HumanTargetOptions; public class PlayModeView extends BorderPane { @FXML private Button backButton; @FXML private VBox sidePane; @FXML private Pane navigationPane; private final GameBoardView boardView; private final HumanActionPromptView actionPromptView; private final LoadingBoardView loadingView; private boolean firstUpdate = true; public PlayModeView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/PlayModeView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } boardView = new GameBoardView(); // setCenter(boardView); loadingView = new LoadingBoardView(); setCenter(loadingView); actionPromptView = new HumanActionPromptView(); //sidePane.getChildren().add(actionPromptView); backButton.setOnAction(actionEvent -> NotificationProxy.sendNotification(GameNotification.MAIN_MENU)); sidePane.getChildren().setAll(actionPromptView, navigationPane); } public void disableTargetSelection() { boardView.disableTargetSelection(); actionPromptView.setVisible(true); } public void enableTargetSelection(HumanTargetOptions targetOptions) { boardView.enableTargetSelection(targetOptions); } public HumanActionPromptView getActionPromptView() { return actionPromptView; } public void showAnimations(GameContext context) { boardView.showAnimations(context); } public void updateGameState(GameContext context) { if (firstUpdate) { setCenter(boardView); firstUpdate = false; } boardView.updateGameState(context); if (context.gameDecided()) { sidePane.getChildren().clear(); sidePane.getChildren().add(backButton); } } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/StartGameCommand.java ================================================ package net.demilich.metastone.gui.playmode; import net.demilich.metastone.NotificationProxy; 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.decks.DeckFormat; import net.demilich.metastone.game.logic.GameLogic; import net.demilich.metastone.game.gameconfig.GameConfig; import net.demilich.metastone.game.gameconfig.PlayerConfig; public class StartGameCommand extends SimpleCommand { @Override public void execute(INotification notification) { GameConfig gameConfig = (GameConfig) notification.getBody(); PlayerConfig playerConfig1 = gameConfig.getPlayerConfig1(); PlayerConfig playerConfig2 = gameConfig.getPlayerConfig2(); Player player1 = new Player(playerConfig1); Player player2 = new Player(playerConfig2); DeckFormat deckFormat = gameConfig.getDeckFormat(); GameContext newGame = new GameContextVisualizable(player1, player2, new GameLogic(), deckFormat); Thread t = new Thread(new Runnable() { @Override public void run() { NotificationProxy.sendNotification(GameNotification.PLAY_GAME, newGame); } }); t.setDaemon(true); t.start(); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/SummonToken.java ================================================ package net.demilich.metastone.gui.playmode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javafx.fxml.FXML; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.shape.Shape; import javafx.scene.text.Text; import net.demilich.metastone.game.Attribute; import net.demilich.metastone.game.entities.minions.Minion; import net.demilich.metastone.game.entities.minions.Permanent; import net.demilich.metastone.game.entities.minions.Summon; import net.demilich.metastone.gui.cards.CardTooltip; public class SummonToken extends GameToken { @FXML private Label name; @FXML private Group attackAnchor; @FXML private Group hpAnchor; @FXML private Node defaultToken; @FXML private Node divineShield; @FXML private Node taunt; @FXML private Text windfury; @FXML private Node deathrattle; @FXML private Shape frozen; private CardTooltip cardTooltip; Logger logger = LoggerFactory.getLogger(SummonToken.class); public SummonToken() { super("SummonToken.fxml"); Tooltip tooltip = new Tooltip(); cardTooltip = new CardTooltip(); tooltip.setGraphic(cardTooltip); Tooltip.install(this, tooltip); frozen.getStrokeDashArray().add(16.0); } public void setSummon(Summon summon) { name.setText(summon.getName()); if (summon instanceof Minion) { attackAnchor.setVisible(true); hpAnchor.setVisible(true); setScoreValue(attackAnchor, summon.getAttack(), summon.getAttributeValue(Attribute.BASE_ATTACK)); setScoreValue(hpAnchor, summon.getHp(), summon.getBaseHp(), summon.getMaxHp()); } else if (summon instanceof Permanent) { attackAnchor.setVisible(false); hpAnchor.setVisible(false); } visualizeStatus(summon); cardTooltip.setCard(summon.getSourceCard()); } private void visualizeStatus(Summon summon) { taunt.setVisible(summon.hasAttribute(Attribute.TAUNT)); defaultToken.setVisible(!summon.hasAttribute(Attribute.TAUNT)); divineShield.setVisible(summon.hasAttribute(Attribute.DIVINE_SHIELD)); windfury.setVisible(summon.hasAttribute(Attribute.WINDFURY) || summon.hasAttribute(Attribute.MEGA_WINDFURY)); if(summon.hasAttribute(Attribute.MEGA_WINDFURY)) { windfury.setText("x4"); } else { windfury.setText("x2"); } deathrattle.setVisible(summon.hasAttribute(Attribute.DEATHRATTLES)); frozen.setVisible(summon.hasAttribute(Attribute.FROZEN)); visualizeStealth(summon); } private void visualizeStealth(Summon summon) { Node token = summon.hasAttribute(Attribute.TAUNT) ? taunt : defaultToken; token.setOpacity(summon.hasAttribute(Attribute.STEALTH) ? 0.5 : 1); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/AnimationCompletedCommand.java ================================================ package net.demilich.metastone.gui.playmode.animation; import net.demilich.nittygrittymvc.SimpleCommand; import net.demilich.nittygrittymvc.interfaces.INotification; import net.demilich.metastone.GameNotification; public class AnimationCompletedCommand extends SimpleCommand { @Override public void execute(INotification notification) { AnimationProxy animationProxy = (AnimationProxy) getFacade().retrieveProxy(AnimationProxy.NAME); animationProxy.animationCompleted(); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/AnimationLockCommand.java ================================================ package net.demilich.metastone.gui.playmode.animation; import net.demilich.nittygrittymvc.SimpleCommand; import net.demilich.nittygrittymvc.interfaces.INotification; import net.demilich.metastone.GameNotification; import net.demilich.metastone.gui.playmode.GameContextVisualizable; public class AnimationLockCommand extends SimpleCommand { @Override public void execute(INotification notification) { AnimationProxy animationProxy = (AnimationProxy) getFacade().retrieveProxy(AnimationProxy.NAME); GameContextVisualizable contextVisualizable = (GameContextVisualizable) notification.getBody(); animationProxy.setContext(contextVisualizable); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/AnimationProxy.java ================================================ package net.demilich.metastone.gui.playmode.animation; import net.demilich.nittygrittymvc.Proxy; import net.demilich.metastone.GameNotification; import net.demilich.metastone.gui.playmode.GameContextVisualizable; public class AnimationProxy extends Proxy { public static final String NAME = "AnimationProxy"; private GameContextVisualizable context; private int animationsRunning; public AnimationProxy() { super(NAME); } public void animationCompleted() { if (--animationsRunning == 0) { context.setBlockedByAnimation(false); } } public void animationStarted() { animationsRunning++; } public GameContextVisualizable getContext() { return context; } public void setContext(GameContextVisualizable context) { this.context = context; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/AnimationStartedCommand.java ================================================ package net.demilich.metastone.gui.playmode.animation; import net.demilich.nittygrittymvc.SimpleCommand; import net.demilich.nittygrittymvc.interfaces.INotification; import net.demilich.metastone.GameNotification; public class AnimationStartedCommand extends SimpleCommand { @Override public void execute(INotification notification) { AnimationProxy animationProxy = (AnimationProxy) getFacade().retrieveProxy(AnimationProxy.NAME); animationProxy.animationStarted(); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/CardPlayedToken.java ================================================ package net.demilich.metastone.gui.playmode.animation; import javafx.animation.FadeTransition; import javafx.event.ActionEvent; import javafx.stage.Popup; import javafx.stage.Window; import javafx.util.Duration; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.cards.Card; import net.demilich.metastone.gui.cards.CardTooltip; import net.demilich.metastone.gui.playmode.GameBoardView; public class CardPlayedToken { private final Popup popup; private final CardTooltip cardToken; public CardPlayedToken(GameBoardView boardView, Card card) { Window parent = boardView.getScene().getWindow(); this.cardToken = new CardTooltip(); popup = new Popup(); popup.getContent().setAll(cardToken); popup.setX(parent.getX() + 40); popup.show(parent); popup.setY(parent.getY() + parent.getHeight() * 0.5 - cardToken.getHeight() * 0.5); cardToken.setCard(card); NotificationProxy.sendNotification(GameNotification.ANIMATION_STARTED); FadeTransition animation = new FadeTransition(Duration.seconds(1.2), cardToken); animation.setDelay(Duration.seconds(0.6f)); animation.setOnFinished(this::onComplete); animation.setFromValue(1); animation.setToValue(0); animation.play(); } private void onComplete(ActionEvent event) { popup.hide(); NotificationProxy.sendNotification(GameNotification.ANIMATION_COMPLETED); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/CardRevealedToken.java ================================================ package net.demilich.metastone.gui.playmode.animation; import javafx.animation.FadeTransition; import javafx.event.ActionEvent; import javafx.stage.Popup; import javafx.stage.Window; import javafx.util.Duration; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.cards.Card; import net.demilich.metastone.gui.cards.CardTooltip; import net.demilich.metastone.gui.playmode.GameBoardView; public class CardRevealedToken { private final Popup popup; private final CardTooltip cardToken; public CardRevealedToken(GameBoardView boardView, Card card, double delay) { Window parent = boardView.getScene().getWindow(); this.cardToken = new CardTooltip(); popup = new Popup(); popup.getContent().setAll(cardToken); popup.setX(parent.getX() + 40); popup.show(parent); popup.setY(parent.getY() + parent.getHeight() * 0.5 - cardToken.getHeight() * 0.5); cardToken.setCard(card); NotificationProxy.sendNotification(GameNotification.ANIMATION_STARTED); FadeTransition animation = new FadeTransition(Duration.seconds(delay), cardToken); animation.setOnFinished(this::secondTransition); animation.setFromValue(0); animation.setToValue(0); animation.play(); } private void secondTransition(ActionEvent event) { FadeTransition animation = new FadeTransition(Duration.seconds(.6), cardToken); animation.setOnFinished(this::nextTransition); animation.setFromValue(0); animation.setToValue(1); animation.play(); } private void nextTransition(ActionEvent event) { FadeTransition animation = new FadeTransition(Duration.seconds(.6), cardToken); animation.setOnFinished(this::onComplete); animation.setFromValue(1); animation.setToValue(0); animation.play(); } private void onComplete(ActionEvent event) { popup.hide(); NotificationProxy.sendNotification(GameNotification.ANIMATION_COMPLETED); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/DamageEventVisualizer.java ================================================ package net.demilich.metastone.gui.playmode.animation; import java.util.HashMap; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.events.DamageEvent; import net.demilich.metastone.game.events.GameEvent; import net.demilich.metastone.gui.playmode.GameBoardView; import net.demilich.metastone.gui.playmode.GameToken; public class DamageEventVisualizer implements IGameEventVisualizer { private HashMap recentHits = new HashMap<>(); @Override public void visualizeEvent(GameContext gameContext, GameEvent event, GameBoardView boardView) { DamageEvent damageEvent = (DamageEvent) event; GameToken targetToken = boardView.getToken(damageEvent.getVictim()); if (targetToken == null) { return; } Integer victimId = damageEvent.getVictim().getId(); if (!recentHits.containsKey(victimId)) { recentHits.put(victimId, new HitInfo()); } // when the last displayed hit was on the same target and only a small // amount of time passed, offset // the damage numbers so that all are actually visible HitInfo hitInfo = recentHits.get(victimId); if (System.currentTimeMillis() - hitInfo.lastHitTime < 1000) { hitInfo.successiveHits++; } else { hitInfo.successiveHits = 0; } new DamageNumber("-" + damageEvent.getDamage(), targetToken, hitInfo.successiveHits); hitInfo.lastHitTime = System.currentTimeMillis(); } private class HitInfo { public long lastHitTime; public int successiveHits; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/DamageNumber.java ================================================ package net.demilich.metastone.gui.playmode.animation; import javafx.animation.PauseTransition; import javafx.event.ActionEvent; import javafx.geometry.Pos; import javafx.scene.CacheHint; import javafx.scene.image.ImageView; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.text.Text; import javafx.util.Duration; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.gui.IconFactory; import net.demilich.metastone.gui.playmode.GameToken; public class DamageNumber extends StackPane { private final GameToken parent; public DamageNumber(String text, GameToken parent, int successiveHits) { this.parent = parent; this.setAlignment(Pos.CENTER); ImageView image = new ImageView(IconFactory.getImageUrl("common/splash.png")); image.setFitWidth(96); image.setFitHeight(96); if (successiveHits > 0) { double xOffset = -48 * successiveHits; setTranslateX(xOffset); } Text textShape = new Text(text); textShape.setFill(Color.WHITE); textShape.setStyle("-fx-font-size: 22pt; -fx-font-family: \"System\";-fx-font-weight: 900;-fx-stroke: black;-fx-stroke-width: 2;"); setCache(true); setCacheHint(CacheHint.SPEED); getChildren().add(image); getChildren().add(textShape); parent.getAnchor().getChildren().add(this); NotificationProxy.sendNotification(GameNotification.ANIMATION_STARTED); PauseTransition animation = new PauseTransition(Duration.seconds(1.2)); animation.setOnFinished(this::onComplete); animation.play(); } private void onComplete(ActionEvent event) { parent.getAnchor().getChildren().remove(this); NotificationProxy.sendNotification(GameNotification.ANIMATION_COMPLETED); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/EventVisualizerDispatcher.java ================================================ package net.demilich.metastone.gui.playmode.animation; import java.util.HashMap; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.events.GameEvent; import net.demilich.metastone.game.events.GameEventType; import net.demilich.metastone.gui.playmode.GameBoardView; import net.demilich.metastone.gui.playmode.GameContextVisualizable; public class EventVisualizerDispatcher { private static final HashMap visualizers = new HashMap(); static { visualizers.put(GameEventType.DAMAGE, new DamageEventVisualizer()); visualizers.put(GameEventType.HEAL, new HealEventVisualizer()); visualizers.put(GameEventType.PLAY_CARD, new PlayCardVisualizer()); visualizers.put(GameEventType.JOUST, new JoustVisualizer()); visualizers.put(GameEventType.REVEAL_CARD, new RevealCardVisualizer()); } public void visualize(GameContextVisualizable gameContext, GameBoardView boardView) { NotificationProxy.sendNotification(GameNotification.ANIMATION_STARTED); for (GameEvent event : gameContext.getGameEvents()) { IGameEventVisualizer gameEventVisualizer = visualizers.get(event.getEventType()); if (gameEventVisualizer == null) { continue; } gameEventVisualizer.visualizeEvent(gameContext, event, boardView); } gameContext.getGameEvents().clear(); NotificationProxy.sendNotification(GameNotification.ANIMATION_COMPLETED); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/HealEventVisualizer.java ================================================ package net.demilich.metastone.gui.playmode.animation; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.events.GameEvent; import net.demilich.metastone.game.events.HealEvent; import net.demilich.metastone.gui.playmode.GameBoardView; import net.demilich.metastone.gui.playmode.GameToken; public class HealEventVisualizer implements IGameEventVisualizer { @Override public void visualizeEvent(GameContext gameContext, GameEvent event, GameBoardView boardView) { HealEvent healEvent = (HealEvent) event; GameToken targetToken = boardView.getToken(healEvent.getEventTarget()); if (targetToken == null) { return; } new HealingNumber("+" + healEvent.getHealing(), targetToken); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/HealingNumber.java ================================================ package net.demilich.metastone.gui.playmode.animation; import javafx.animation.TranslateTransition; import javafx.event.ActionEvent; import javafx.scene.CacheHint; import javafx.scene.paint.Color; import javafx.scene.text.Text; import javafx.util.Duration; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.gui.playmode.GameToken; public class HealingNumber extends Text { private final GameToken parent; public HealingNumber(String text, GameToken parent) { this.parent = parent; setText(text); setFill(Color.GREEN); setStyle("-fx-font-size: 28pt; -fx-font-family: \"System\";-fx-font-weight: bolder;-fx-stroke: black;-fx-stroke-width: 2;"); setCache(true); setCacheHint(CacheHint.SPEED); parent.getAnchor().getChildren().add(this); NotificationProxy.sendNotification(GameNotification.ANIMATION_STARTED); TranslateTransition animation = new TranslateTransition(Duration.seconds(0.5), this); animation.setToY(-30); animation.setOnFinished(this::onComplete); animation.play(); } private void onComplete(ActionEvent event) { parent.getAnchor().getChildren().remove(this); NotificationProxy.sendNotification(GameNotification.ANIMATION_COMPLETED); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/IAnimationListener.java ================================================ package net.demilich.metastone.gui.playmode.animation; public interface IAnimationListener { public void animationCompleted(); public void animationStarted(); } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/IGameEventVisualizer.java ================================================ package net.demilich.metastone.gui.playmode.animation; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.events.GameEvent; import net.demilich.metastone.gui.playmode.GameBoardView; public interface IGameEventVisualizer { void visualizeEvent(GameContext gameContext, GameEvent event, GameBoardView boardView); } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/JoustToken.java ================================================ package net.demilich.metastone.gui.playmode.animation; import javafx.animation.FadeTransition; import javafx.animation.ScaleTransition; import javafx.event.ActionEvent; import javafx.stage.Popup; import javafx.stage.Window; import javafx.util.Duration; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.cards.Card; import net.demilich.metastone.gui.cards.CardTooltip; import net.demilich.metastone.gui.playmode.GameBoardView; public class JoustToken { private final Popup popup; private final CardTooltip cardToken; public JoustToken(GameBoardView boardView, Card card, boolean up, boolean won) { Window parent = boardView.getScene().getWindow(); this.cardToken = new CardTooltip(); popup = new Popup(); popup.getContent().setAll(cardToken); popup.setX(parent.getX() + 600); popup.show(parent); int offsetY = up ? -200 : 100; popup.setY(parent.getY() + parent.getHeight() * 0.5 - cardToken.getHeight() * 0.5 + offsetY); cardToken.setCard(card); NotificationProxy.sendNotification(GameNotification.ANIMATION_STARTED); FadeTransition animation = new FadeTransition(Duration.seconds(1.0), cardToken); animation.setDelay(Duration.seconds(1f)); animation.setOnFinished(this::onComplete); animation.setFromValue(1); animation.setToValue(0); animation.play(); if (won) { ScaleTransition scaleAnimation = new ScaleTransition(Duration.seconds(0.5f), cardToken); scaleAnimation.setByX(0.1); scaleAnimation.setByY(0.1); scaleAnimation.setCycleCount(2); scaleAnimation.setAutoReverse(true); scaleAnimation.play(); } } private void onComplete(ActionEvent event) { popup.hide(); NotificationProxy.sendNotification(GameNotification.ANIMATION_COMPLETED); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/JoustVisualizer.java ================================================ package net.demilich.metastone.gui.playmode.animation; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.cards.Card; import net.demilich.metastone.game.events.GameEvent; import net.demilich.metastone.game.events.JoustEvent; import net.demilich.metastone.gui.playmode.GameBoardView; public class JoustVisualizer implements IGameEventVisualizer { @Override public void visualizeEvent(GameContext gameContext, GameEvent event, GameBoardView boardView) { JoustEvent joustEvent = (JoustEvent) event; Card downCard = null; Card upCard = null; boolean upWon = false; if (joustEvent.getTargetPlayerId() == GameContext.PLAYER_1) { downCard = joustEvent.getOwnCard(); upCard = joustEvent.getOpponentCard(); upWon = !joustEvent.isWon(); } else { downCard = joustEvent.getOpponentCard(); upCard = joustEvent.getOwnCard(); upWon = joustEvent.isWon(); } if (upCard != null) { new JoustToken(boardView, upCard, true, upWon); } if (downCard != null) { new JoustToken(boardView, downCard, false, !upWon); } } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/PlayCardVisualizer.java ================================================ package net.demilich.metastone.gui.playmode.animation; import net.demilich.metastone.game.Attribute; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.events.CardPlayedEvent; import net.demilich.metastone.game.events.GameEvent; import net.demilich.metastone.gui.playmode.GameBoardView; public class PlayCardVisualizer implements IGameEventVisualizer { @Override public void visualizeEvent(GameContext gameContext, GameEvent event, GameBoardView boardView) { CardPlayedEvent cardPlayedEvent = (CardPlayedEvent) event; if (cardPlayedEvent.getCard().hasAttribute(Attribute.SECRET)) { return; } new CardPlayedToken(boardView, cardPlayedEvent.getCard()); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/animation/RevealCardVisualizer.java ================================================ package net.demilich.metastone.gui.playmode.animation; import net.demilich.metastone.game.Attribute; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.events.CardRevealedEvent; import net.demilich.metastone.game.events.GameEvent; import net.demilich.metastone.gui.playmode.GameBoardView; public class RevealCardVisualizer implements IGameEventVisualizer { @Override public void visualizeEvent(GameContext gameContext, GameEvent event, GameBoardView boardView) { CardRevealedEvent cardRevealedEvent = (CardRevealedEvent) event; if (cardRevealedEvent.getCard().hasAttribute(Attribute.SECRET)) { return; } new CardRevealedToken(boardView, cardRevealedEvent.getCard(), cardRevealedEvent.getDelay()); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/config/PlayModeConfigMediator.java ================================================ package net.demilich.metastone.gui.playmode.config; 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.decks.Deck; import net.demilich.metastone.game.decks.DeckFormat; import net.demilich.metastone.game.gameconfig.GameConfig; import net.demilich.metastone.gui.playmode.PlayModeMediator; public class PlayModeConfigMediator extends Mediator { public static final String NAME = "PlayModeConfigMediator"; private final PlayModeConfigView view; public PlayModeConfigMediator() { super(NAME); view = new PlayModeConfigView(); } @Override public void handleNotification(final INotification notification) { switch (notification.getId()) { case REPLY_DECKS: @SuppressWarnings("unchecked") List decks = (List) notification.getBody(); view.injectDecks(decks); break; case REPLY_DECK_FORMATS: @SuppressWarnings("unchecked") List deckFormats = (List) notification.getBody(); view.injectDeckFormats(deckFormats); break; case COMMIT_PLAYMODE_CONFIG: getFacade().registerMediator(new PlayModeMediator()); new Thread(new Runnable() { @Override public void run() { GameConfig gameConfig = (GameConfig) notification.getBody(); getFacade().sendNotification(GameNotification.START_GAME, gameConfig); } }).start(); 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.COMMIT_PLAYMODE_CONFIG); return notificationInterests; } @Override public void onRegister() { getFacade().sendNotification(GameNotification.SHOW_VIEW, view); getFacade().sendNotification(GameNotification.REQUEST_DECKS); getFacade().sendNotification(GameNotification.REQUEST_DECK_FORMATS); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/config/PlayModeConfigView.java ================================================ package net.demilich.metastone.gui.playmode.config; import java.io.IOException; import java.util.List; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; 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.ComboBox; import javafx.scene.layout.BorderPane; 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.game.decks.DeckFormat; import net.demilich.metastone.gui.common.DeckFormatStringConverter; import net.demilich.metastone.game.gameconfig.GameConfig; import net.demilich.metastone.gui.gameconfig.PlayerConfigView; public class PlayModeConfigView extends BorderPane implements EventHandler { @FXML protected ComboBox formatBox; @FXML protected HBox playerArea; @FXML protected Button startButton; @FXML protected Button backButton; protected PlayerConfigView player1Config; protected PlayerConfigView player2Config; private List deckFormats; public PlayModeConfigView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/PlayModeConfigView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } formatBox.setConverter(new DeckFormatStringConverter()); player1Config = new PlayerConfigView(PlayerConfigType.HUMAN); player2Config = new PlayerConfigView(PlayerConfigType.OPPONENT); playerArea.getChildren().add(player1Config); playerArea.getChildren().add(player2Config); startButton.setOnAction(this); backButton.setOnAction(this); formatBox.valueProperty().addListener((ChangeListener) (observableProperty, oldDeckFormat, newDeckFormat) -> { setDeckFormats(newDeckFormat); }); } private void setupDeckFormats() { ObservableList deckFormatList = FXCollections.observableArrayList(); for (DeckFormat deckFormat : deckFormats) { deckFormatList.add(deckFormat); } formatBox.setItems(deckFormatList); formatBox.getSelectionModel().selectFirst(); } private void setDeckFormats(DeckFormat newDeckFormat) { player1Config.setDeckFormat(newDeckFormat); player2Config.setDeckFormat(newDeckFormat); } @Override public void handle(ActionEvent actionEvent) { if (actionEvent.getSource() == startButton) { GameConfig gameConfig = new GameConfig(); gameConfig.setNumberOfGames(1); gameConfig.setPlayerConfig1(player1Config.getPlayerConfig()); gameConfig.setPlayerConfig2(player2Config.getPlayerConfig()); gameConfig.setDeckFormat(formatBox.getValue()); NotificationProxy.sendNotification(GameNotification.COMMIT_PLAYMODE_CONFIG, gameConfig); } else if (actionEvent.getSource() == backButton) { NotificationProxy.sendNotification(GameNotification.MAIN_MENU); } } public void injectDecks(List decks) { player1Config.injectDecks(decks); player2Config.injectDecks(decks); } public void injectDeckFormats(List deckFormats) { this.deckFormats = deckFormats; setupDeckFormats(); player1Config.setDeckFormat(formatBox.getValue()); player2Config.setDeckFormat(formatBox.getValue()); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/config/PlayerConfigType.java ================================================ package net.demilich.metastone.gui.playmode.config; public enum PlayerConfigType { HUMAN, OPPONENT, SIMULATION, SANDBOX } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/config/RequestDeckFormatsCommand.java ================================================ package net.demilich.metastone.gui.playmode.config; import java.util.List; import net.demilich.nittygrittymvc.SimpleCommand; import net.demilich.nittygrittymvc.interfaces.INotification; import net.demilich.metastone.GameNotification; import net.demilich.metastone.game.decks.DeckFormat; import net.demilich.metastone.gui.deckbuilder.DeckFormatProxy; public class RequestDeckFormatsCommand extends SimpleCommand { @Override public void execute(INotification notification) { DeckFormatProxy deckFormatProxy = (DeckFormatProxy) getFacade().retrieveProxy(DeckFormatProxy.NAME); getFacade().sendNotification(GameNotification.LOAD_DECK_FORMATS); List deckFormats = deckFormatProxy.getDeckFormats(); getFacade().sendNotification(GameNotification.REPLY_DECK_FORMATS, deckFormats); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/playmode/config/RequestDecksCommand.java ================================================ package net.demilich.metastone.gui.playmode.config; import java.util.List; 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.DeckProxy; public class RequestDecksCommand extends SimpleCommand { @Override public void execute(INotification notification) { DeckProxy deckProxy = (DeckProxy) getFacade().retrieveProxy(DeckProxy.NAME); getFacade().sendNotification(GameNotification.LOAD_DECKS); List decks = deckProxy.getDecks(); getFacade().sendNotification(GameNotification.REPLY_DECKS, decks); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/CardCollectionEditor.java ================================================ package net.demilich.metastone.gui.sandboxmode; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.TextField; import javafx.scene.control.cell.TextFieldListCell; import javafx.util.StringConverter; 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.CardType; public class CardCollectionEditor extends SandboxEditor { private class CardStringConverter extends StringConverter { @Override public Card fromString(String arg0) { return null; } @Override public String toString(Card card) { String result = card.getName(); result += " [" + card.getCardType() + "] "; result += "Mana: " + card.getBaseManaCost(); return result; } } @FXML private Label cardCountLabel; @FXML private ListView editableListView; @FXML private ListView catalogueListView; @FXML private TextField filterTextfield; @FXML private Button clearFilterButton; @FXML private Button addCardButton; @FXML private Button removeCardButton; private ICardCollectionEditingListener listener; private int cardLimit; public CardCollectionEditor(String title, CardCollection cardCollection, ICardCollectionEditingListener listener, int cardLimit) { super("CardCollectionEditor.fxml"); this.listener = listener; this.cardLimit = cardLimit; setTitle(title); editableListView.setCellFactory(TextFieldListCell.forListView(new CardStringConverter())); populateEditableView(cardCollection); catalogueListView.setCellFactory(TextFieldListCell.forListView(new CardStringConverter())); populateCatalogueView(null); filterTextfield.textProperty().addListener(this::onFilterTextChanged); clearFilterButton.setOnAction(actionEvent -> filterTextfield.clear()); okButton.setOnAction(this::handleOkButton); cancelButton.setOnAction(this::handleCancelButton); addCardButton.setOnAction(this::handleAddCardButton); removeCardButton.setOnAction(this::handleRemoveCardButton); } private void handleAddCardButton(ActionEvent actionEvent) { for (Card selectedCard : catalogueListView.getSelectionModel().getSelectedItems()) { editableListView.getItems().add(selectedCard.clone()); } } private void handleCancelButton(ActionEvent actionEvent) { this.getScene().getWindow().hide(); } private void handleEditableCardListChanged(Change change) { int count = editableListView.getItems().size(); cardCountLabel.setText("Cards in collection: " + count + "/" + cardLimit); addCardButton.setDisable(count >= cardLimit); } private void handleOkButton(ActionEvent actionEvent) { CardCollection changedCollection = new CardCollection(); for (Card card : editableListView.getItems()) { changedCollection.add(card); } listener.onFinishedEditing(changedCollection); this.getScene().getWindow().hide(); } private void handleRemoveCardButton(ActionEvent actionEvent) { editableListView.getItems().remove(editableListView.getSelectionModel().getSelectedItem()); } private void onFilterTextChanged(ObservableValue observable, String oldValue, String newValue) { populateCatalogueView(newValue); } private void populateCatalogueView(String filter) { ObservableList data = FXCollections.observableArrayList(); for (Card card : CardCatalogue.getAll()) { if (card.getCardType().isCardType(CardType.HERO)) { continue; } if (filter == null || card.matchesFilter(filter)) { data.add(card); } } catalogueListView.setItems(data); } private void populateEditableView(CardCollection cardCollection) { ObservableList data = FXCollections.observableArrayList(); for (Card card : cardCollection) { data.add(card); } editableListView.setItems(data); data.addListener(this::handleEditableCardListChanged); handleEditableCardListChanged(null); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/CardPanel.java ================================================ package net.demilich.metastone.gui.sandboxmode; import java.io.IOException; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Button; import javafx.scene.layout.VBox; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.Player; import net.demilich.metastone.game.cards.CardCollection; import net.demilich.metastone.game.logic.GameLogic; public class CardPanel extends VBox { @FXML private Button editHandButton; @FXML private Button editDeckButton; private Player selectedPlayer; public CardPanel() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/CardPanel.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } editHandButton.setOnAction(this::handleEditHandButton); editDeckButton.setOnAction(this::handleEditDeckButton); } private void handleEditDeckButton(ActionEvent actionEvent) { CardCollection deck = selectedPlayer.getDeck(); CardCollectionEditor cardCollectionEditor = new CardCollectionEditor("Edit deck", deck, this::onDeckFinishedEditing, GameLogic.MAX_DECK_SIZE); NotificationProxy.sendNotification(GameNotification.SHOW_MODAL_DIALOG, cardCollectionEditor); } private void handleEditHandButton(ActionEvent actionEvent) { CardCollection hand = selectedPlayer.getHand(); CardCollectionEditor cardCollectionEditor = new CardCollectionEditor("Edit hand", hand, this::onHandFinishedEditing, GameLogic.MAX_HAND_CARDS); NotificationProxy.sendNotification(GameNotification.SHOW_MODAL_DIALOG, cardCollectionEditor); } private void onDeckFinishedEditing(CardCollection cardCollection) { NotificationProxy.sendNotification(GameNotification.MODIFY_PLAYER_DECK, cardCollection); } private void onHandFinishedEditing(CardCollection cardCollection) { NotificationProxy.sendNotification(GameNotification.MODIFY_PLAYER_HAND, cardCollection); } public void onPlayerSelectionChanged(Player selectedPlayer) { this.selectedPlayer = selectedPlayer; editHandButton.setDisable(selectedPlayer == null); editDeckButton.setDisable(selectedPlayer == null); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/EntityEditor.java ================================================ package net.demilich.metastone.gui.sandboxmode; import java.util.EnumMap; import java.util.Map; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.util.Callback; import net.demilich.metastone.game.Attribute; import net.demilich.metastone.game.entities.Entity; import net.demilich.metastone.game.utils.TagValueType; import net.demilich.metastone.gui.common.IntegerTextField; import net.demilich.metastone.utils.ICallback; public class EntityEditor extends SandboxEditor { private class PairKeyFactory implements Callback, ObservableValue> { @Override public ObservableValue call(TableColumn.CellDataFeatures data) { return new ReadOnlyObjectWrapper<>(data.getValue().getName()); } } private class PairValueCell extends TableCell { @Override protected void updateItem(Object item, boolean empty) { super.updateItem(item, empty); if (item == null) { return; } GameTagEntry entry = (GameTagEntry) item; TagValueType tagValueType = entry.getValueType(); Attribute tag = entry.getTag(); if (tagValueType == TagValueType.INTEGER) { IntegerTextField numericTextfield = getNumericTextField(); numericTextfield.setIntValue(entry.getValueInt()); numericTextfield.valueProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observableValue, Number oldValue, Number newValue) { workingCopy.put(tag, numericTextfield.getIntValue()); } }); setGraphic(numericTextfield); setText(null); } else if (tagValueType == TagValueType.BOOLEAN) { CheckBox checkBox = new CheckBox(); checkBox.setSelected(entry.getValueBool()); checkBox.selectedProperty().addListener(new ChangeListener() { public void changed(ObservableValue observableValue, Boolean oldValue, Boolean newValue) { if (checkBox.isSelected()) { workingCopy.put(tag, 1); } else { workingCopy.remove(tag); } } }); setGraphic(checkBox); setText(null); } else { setGraphic(null); setText(entry.getValue().toString()); } } } private class PairValueFactory implements Callback, ObservableValue> { @Override public ObservableValue call(TableColumn.CellDataFeatures data) { return new ReadOnlyObjectWrapper<>(data.getValue()); } } private final Map workingCopy = new EnumMap(Attribute.class); private final Entity entity; @FXML private TableView propertiesTable; @FXML private TableColumn nameColumn; @FXML private TableColumn valueColumn; private final ICallback callback; public EntityEditor(Entity entity, ICallback callback) { super("EntityEditor.fxml"); this.entity = entity; this.callback = callback; setTitle("Edit " + entity.getName()); okButton.setOnAction(this::handleOkButton); cancelButton.setOnAction(this::handleCancelButton); nameColumn.setCellValueFactory(new PairKeyFactory()); valueColumn.setCellValueFactory(new PairValueFactory()); valueColumn.setCellFactory(new Callback, TableCell>() { @Override public TableCell call(TableColumn column) { return new PairValueCell(); } }); addTagsIfMissing(entity); populateTable(entity); } private void addTagIfMissing(Entity entity, Attribute tag, Object defaultValue) { if (entity.hasAttribute(tag)) { return; } entity.setAttribute(tag, defaultValue); } private void addTagsIfMissing(Entity entity) { switch (entity.getEntityType()) { case CARD: break; case HERO: addTagIfMissing(entity, Attribute.ARMOR, 0); break; case MINION: addTagIfMissing(entity, Attribute.DIVINE_SHIELD, 0); addTagIfMissing(entity, Attribute.WINDFURY, 0); addTagIfMissing(entity, Attribute.FROZEN, 0); addTagIfMissing(entity, Attribute.TEMPORARY_ATTACK_BONUS, 0); addTagIfMissing(entity, Attribute.HP_BONUS, 0); addTagIfMissing(entity, Attribute.ATTACK_BONUS, 0); addTagIfMissing(entity, Attribute.CHARGE, 0); addTagIfMissing(entity, Attribute.STEALTH, 0); addTagIfMissing(entity, Attribute.TAUNT, 0); break; case WEAPON: break; default: break; } } private IntegerTextField getNumericTextField() { IntegerTextField textField = new IntegerTextField(3); textField.setMaxWidth(100); return textField; } private void handleCancelButton(ActionEvent actionEvent) { this.getScene().getWindow().hide(); } private void handleOkButton(ActionEvent actionEvent) { entity.getAttributes().clear(); for (Attribute tag : workingCopy.keySet()) { entity.setAttribute(tag, workingCopy.get(tag)); } this.getScene().getWindow().hide(); if (callback != null) { callback.call(entity); } } private void populateTable(Entity entity) { Map tags = entity.getAttributes(); ObservableList data = FXCollections.observableArrayList(); for (Attribute tag : tags.keySet()) { Object value = tags.get(tag); workingCopy.put(tag, value); GameTagEntry entry = new GameTagEntry(tag, value); data.add(entry); } propertiesTable.getItems().setAll(data); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/GameTagEntry.java ================================================ package net.demilich.metastone.gui.sandboxmode; import net.demilich.metastone.game.Attribute; import net.demilich.metastone.game.utils.GameTagUtils; import net.demilich.metastone.game.utils.TagValueType; public class GameTagEntry { private final Attribute tag; private final TagValueType valueType; private final Object value; public GameTagEntry(Attribute tag, Object value) { this.tag = tag; this.value = value; this.valueType = GameTagUtils.getTagValueType(tag); } public String getName() { return GameTagUtils.getTagName(getTag()); } public Attribute getTag() { return tag; } public Object getValue() { return value; } public boolean getValueBool() { return (int) value >= 1; } public int getValueInt() { return (int) value; } public TagValueType getValueType() { return valueType; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/ICardCollectionEditingListener.java ================================================ package net.demilich.metastone.gui.sandboxmode; import net.demilich.metastone.game.cards.CardCollection; public interface ICardCollectionEditingListener { void onFinishedEditing(CardCollection cardCollection); } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/MinionPanel.java ================================================ package net.demilich.metastone.gui.sandboxmode; import java.io.IOException; import javafx.beans.value.ObservableValue; 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.TextField; import javafx.scene.layout.VBox; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; 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.CardCatalogue; import net.demilich.metastone.game.cards.CardType; import net.demilich.metastone.game.cards.MinionCard; import net.demilich.metastone.gui.sandboxmode.actions.KillAction; import net.demilich.metastone.gui.sandboxmode.actions.SilenceAction; public class MinionPanel extends VBox { @FXML private ComboBox minionComboBox; @FXML private TextField filterMinionsTextField; @FXML private Button spawnMinionButton; @FXML private Button killMinionButton; @FXML private Button silenceButton; public MinionPanel() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/MinionPanel.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } populateMinions(null); filterMinionsTextField.textProperty().addListener(this::onMinionFilterChanged); spawnMinionButton.setOnAction(this::handleSpawnMinionButton); killMinionButton.setOnAction(this::handleKillMinionButton); silenceButton.setOnAction(this::handleSilenceButton); } private void handleKillMinionButton(ActionEvent actionEvent) { KillAction killAction = new KillAction(); NotificationProxy.sendNotification(GameNotification.PERFORM_ACTION, killAction); } private void handleSilenceButton(ActionEvent actionEvent) { SilenceAction silenceAction = new SilenceAction(); NotificationProxy.sendNotification(GameNotification.PERFORM_ACTION, silenceAction); } private void handleSpawnMinionButton(ActionEvent actionEvent) { MinionCard selectedMinion = minionComboBox.getSelectionModel().getSelectedItem(); NotificationProxy.sendNotification(GameNotification.SPAWN_MINION, selectedMinion); } private void onMinionFilterChanged(ObservableValue observable, String oldValue, String newValue) { populateMinions(newValue); } private void populateMinions(String filter) { ObservableList data = FXCollections.observableArrayList(); for (Card card : CardCatalogue.getAll()) { if (!card.getCardType().isCardType(CardType.MINION)) { continue; } if (!card.matchesFilter(filter)) { continue; } MinionCard minionCard = (MinionCard) card; data.add(minionCard); } minionComboBox.setItems(data); minionComboBox.getSelectionModel().selectFirst(); spawnMinionButton.setDisable(minionComboBox.getSelectionModel().getSelectedItem() == null); } public void setContext(GameContext context) { killMinionButton.setDisable(true); for (Player player : context.getPlayers()) { if (context.getSummonCount(player) > 0) { killMinionButton.setDisable(false); break; } } silenceButton.setDisable(killMinionButton.isDisabled()); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/PlayerPanel.java ================================================ package net.demilich.metastone.gui.sandboxmode; import java.io.IOException; import javafx.beans.value.ObservableValue; 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.layout.VBox; import javafx.util.StringConverter; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.Player; import net.demilich.metastone.game.logic.GameLogic; import net.demilich.metastone.gui.sandboxmode.actions.EditEntityAction; import net.demilich.metastone.gui.sandboxmode.actions.SetManaAction; import net.demilich.metastone.gui.sandboxmode.actions.SetMaxManaAction; public class PlayerPanel extends VBox { private class PlayerStringConverter extends StringConverter { @Override public Player fromString(String arg0) { return null; } @Override public String toString(Player player) { return player.getName(); } } @FXML private ComboBox playerComboBox; @FXML private Button editEntityButton; @FXML private ComboBox currentManaBox; @FXML private ComboBox maxManaBox; private boolean ignoreManaChange; private Player selectedPlayer; public PlayerPanel() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/PlayerPanel.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } playerComboBox.setConverter(new PlayerStringConverter()); playerComboBox.getSelectionModel().selectedItemProperty().addListener(this::handlePlayerChanged); currentManaBox.getSelectionModel().selectedItemProperty().addListener(this::handleCurrentManaChanged); maxManaBox.getSelectionModel().selectedItemProperty().addListener(this::handleMaxManaChanged); editEntityButton.setOnAction(this::handleEditEntityButton); } private void handleCurrentManaChanged(ObservableValue ov, Number oldIndex, Number newIndex) { if (selectedPlayer == null || ignoreManaChange) { return; } Integer newValue = currentManaBox.getSelectionModel().getSelectedItem(); SetManaAction setManaAction = new SetManaAction(selectedPlayer.getId(), newValue); NotificationProxy.sendNotification(GameNotification.PERFORM_ACTION, setManaAction); } private void handleEditEntityButton(ActionEvent actionEvent) { EditEntityAction editAction = new EditEntityAction(); NotificationProxy.sendNotification(GameNotification.PERFORM_ACTION, editAction); } private void handleMaxManaChanged(ObservableValue ov, Number oldIndex, Number newIndex) { if (selectedPlayer == null || ignoreManaChange) { return; } Integer newValue = maxManaBox.getSelectionModel().getSelectedItem(); SetMaxManaAction setMaxManaAction = new SetMaxManaAction(selectedPlayer.getId(), newValue); NotificationProxy.sendNotification(GameNotification.PERFORM_ACTION, setMaxManaAction); } private void handlePlayerChanged(ObservableValue ov, Player oldSelected, Player newSelected) { selectedPlayer = newSelected; NotificationProxy.sendNotification(GameNotification.SELECT_PLAYER, selectedPlayer); populateManaBoxes(); } private void populateManaBoxes() { ignoreManaChange = true; currentManaBox.getItems().clear(); for (int i = 0; i <= GameLogic.MAX_MANA; i++) { currentManaBox.getItems().add(i); } currentManaBox.autosize(); maxManaBox.getItems().clear(); for (int i = 0; i <= GameLogic.MAX_MANA; i++) { maxManaBox.getItems().add(i); } currentManaBox.getSelectionModel().select(selectedPlayer.getMana()); maxManaBox.getSelectionModel().select(selectedPlayer.getMaxMana()); ignoreManaChange = false; } public void setContext(GameContext context) { if (playerComboBox.getSelectionModel().isEmpty()) { ObservableList players = FXCollections.observableArrayList(); players.addAll(context.getPlayers()); playerComboBox.setItems(players); playerComboBox.getSelectionModel().selectFirst(); } populateManaBoxes(); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/SandboxEditor.java ================================================ package net.demilich.metastone.gui.sandboxmode; import java.io.IOException; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.BorderPane; public class SandboxEditor extends BorderPane { @FXML protected Label headerLabel; @FXML protected Button okButton; @FXML protected Button cancelButton; public SandboxEditor(String fxmlFile) { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/" + fxmlFile)); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } } protected void setTitle(String title) { headerLabel.setText(title); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/SandboxModeConfigView.java ================================================ package net.demilich.metastone.gui.sandboxmode; import java.io.IOException; import java.util.List; import javafx.beans.value.ChangeListener; 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.layout.BorderPane; 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.game.decks.DeckFormat; import net.demilich.metastone.gui.common.DeckFormatStringConverter; import net.demilich.metastone.game.gameconfig.GameConfig; import net.demilich.metastone.gui.gameconfig.PlayerConfigView; import net.demilich.metastone.gui.playmode.config.PlayerConfigType; public class SandboxModeConfigView extends BorderPane { @FXML protected ComboBox formatBox; @FXML protected HBox playerArea; @FXML protected Button startButton; @FXML protected Button backButton; protected PlayerConfigView player1Config; protected PlayerConfigView player2Config; private List deckFormats; public SandboxModeConfigView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/SandboxModeConfigView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } formatBox.setConverter(new DeckFormatStringConverter()); player1Config = new PlayerConfigView(PlayerConfigType.SANDBOX); player2Config = new PlayerConfigView(PlayerConfigType.SANDBOX); playerArea.getChildren().add(player1Config); playerArea.getChildren().add(player2Config); startButton.setOnAction(this::handleStartButton); backButton.setOnAction(this::handleBackButton); formatBox.valueProperty().addListener((ChangeListener) (observableProperty, oldDeckFormat, newDeckFormat) -> { setDeckFormats(newDeckFormat); }); } private void setupDeckFormats() { ObservableList deckFormatList = FXCollections.observableArrayList(); for (DeckFormat deckFormat : deckFormats) { deckFormatList.add(deckFormat); } formatBox.setItems(deckFormatList); formatBox.getSelectionModel().selectFirst(); } private void setDeckFormats(DeckFormat newDeckFormat) { player1Config.setDeckFormat(newDeckFormat); player2Config.setDeckFormat(newDeckFormat); } private void handleBackButton(ActionEvent event) { NotificationProxy.sendNotification(GameNotification.MAIN_MENU); } private void handleStartButton(ActionEvent event) { GameConfig gameConfig = new GameConfig(); gameConfig.setNumberOfGames(1); gameConfig.setPlayerConfig1(player1Config.getPlayerConfig()); gameConfig.setPlayerConfig2(player2Config.getPlayerConfig()); gameConfig.setDeckFormat(formatBox.getValue()); NotificationProxy.sendNotification(GameNotification.COMMIT_SANDBOXMODE_CONFIG, gameConfig); } public void injectDecks(List decks) { player1Config.injectDecks(decks); player2Config.injectDecks(decks); } public void injectDeckFormats(List deckFormats) { this.deckFormats = deckFormats; setupDeckFormats(); player1Config.setDeckFormat(formatBox.getValue()); player2Config.setDeckFormat(formatBox.getValue()); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/SandboxModeMediator.java ================================================ package net.demilich.metastone.gui.sandboxmode; import java.util.ArrayList; import java.util.List; import net.demilich.nittygrittymvc.Mediator; import net.demilich.nittygrittymvc.interfaces.INotification; import javafx.application.Platform; import javafx.event.EventHandler; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import net.demilich.metastone.GameNotification; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.Player; import net.demilich.metastone.game.behaviour.human.HumanActionOptions; import net.demilich.metastone.game.behaviour.human.HumanTargetOptions; import net.demilich.metastone.game.decks.Deck; import net.demilich.metastone.game.decks.DeckFormat; public class SandboxModeMediator extends Mediatorimplements EventHandler { public static final String NAME = "SandboxModeMediator"; private final SandboxModeConfigView configView; private final SandboxModeView view; public SandboxModeMediator() { super(NAME); configView = new SandboxModeConfigView(); view = new SandboxModeView(); } @Override public void handle(KeyEvent keyEvent) { if (keyEvent.getCode() != KeyCode.ESCAPE) { return; } view.disableTargetSelection(); } @SuppressWarnings("unchecked") @Override public void handleNotification(final INotification notification) { switch (notification.getId()) { case GAME_STATE_LATE_UPDATE: case UPDATE_SANDBOX_STATE: GameContext context = (GameContext) notification.getBody(); Platform.runLater(() -> view.updateSandbox(context)); break; case GAME_STATE_UPDATE: GameContext context2 = (GameContext) notification.getBody(); Platform.runLater(() -> view.showAnimations(context2)); break; case SELECT_TARGET: HumanTargetOptions targetOptions = (HumanTargetOptions) notification.getBody(); Platform.runLater(() -> view.getBoardView().enableTargetSelection(targetOptions)); break; case HUMAN_PROMPT_FOR_ACTION: HumanActionOptions actionOptions = (HumanActionOptions) notification.getBody(); Platform.runLater(() -> view.getActionPromptView().setActions(actionOptions)); break; case HUMAN_PROMPT_FOR_TARGET: HumanTargetOptions options = (HumanTargetOptions) notification.getBody(); Platform.runLater(() -> view.enableTargetSelection(options)); break; case SELECT_PLAYER: view.onPlayerSelectionChanged((Player) notification.getBody()); break; case COMMIT_SANDBOXMODE_CONFIG: getFacade().sendNotification(GameNotification.SHOW_VIEW, view); view.setOnKeyPressed(this); getFacade().sendNotification(GameNotification.CREATE_NEW_SANDBOX, notification.getBody()); break; case REPLY_DECKS: configView.injectDecks((List) notification.getBody()); break; case REPLY_DECK_FORMATS: configView.injectDeckFormats((List) notification.getBody()); break; default: break; } } @Override public List listNotificationInterests() { List notificationInterests = new ArrayList(); notificationInterests.add(GameNotification.UPDATE_SANDBOX_STATE); notificationInterests.add(GameNotification.SELECT_TARGET); notificationInterests.add(GameNotification.HUMAN_PROMPT_FOR_ACTION); notificationInterests.add(GameNotification.HUMAN_PROMPT_FOR_TARGET); notificationInterests.add(GameNotification.GAME_STATE_UPDATE); notificationInterests.add(GameNotification.GAME_STATE_LATE_UPDATE); notificationInterests.add(GameNotification.SELECT_PLAYER); notificationInterests.add(GameNotification.COMMIT_SANDBOXMODE_CONFIG); notificationInterests.add(GameNotification.REPLY_DECK_FORMATS); notificationInterests.add(GameNotification.REPLY_DECKS); return notificationInterests; } @Override public void onRegister() { getFacade().sendNotification(GameNotification.SHOW_VIEW, configView); getFacade().sendNotification(GameNotification.REQUEST_DECKS); getFacade().sendNotification(GameNotification.REQUEST_DECK_FORMATS); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/SandboxModeView.java ================================================ package net.demilich.metastone.gui.sandboxmode; import java.io.IOException; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Button; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.Player; import net.demilich.metastone.game.behaviour.human.HumanTargetOptions; import net.demilich.metastone.gui.IconFactory; import net.demilich.metastone.gui.playmode.GameBoardView; import net.demilich.metastone.gui.playmode.HumanActionPromptView; import net.demilich.metastone.gui.playmode.LoadingBoardView; public class SandboxModeView extends BorderPane { @FXML private Button backButton; @FXML private Button playButton; @FXML private VBox sidebar; @FXML private Pane navigationPane; private final GameBoardView boardView; private final ToolboxView toolboxView; private final HumanActionPromptView actionPromptView; private final LoadingBoardView loadingBoardView; private boolean firstUpdate = true; public SandboxModeView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/SandboxModeView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } boardView = new GameBoardView(); loadingBoardView = new LoadingBoardView(); loadingBoardView.setScaleX(0.9); loadingBoardView.setScaleY(0.9); loadingBoardView.setScaleZ(0.9); setCenter(loadingBoardView); toolboxView = new ToolboxView(); actionPromptView = new HumanActionPromptView(); backButton.setOnAction(actionEvent -> NotificationProxy.sendNotification(GameNotification.MAIN_MENU)); playButton.setOnAction(this::startPlayMode); sidebar.getChildren().setAll(toolboxView, navigationPane); } public void disableTargetSelection() { boardView.disableTargetSelection(); actionPromptView.setVisible(true); } public void enableTargetSelection(HumanTargetOptions targetOptions) { boardView.enableTargetSelection(targetOptions); } public HumanActionPromptView getActionPromptView() { return actionPromptView; } public GameBoardView getBoardView() { return boardView; } public void onPlayerSelectionChanged(Player selectedPlayer) { toolboxView.onPlayerSelectionChanged(selectedPlayer); } public void showAnimations(GameContext context) { getBoardView().showAnimations(context); } private void startPlayMode(ActionEvent actionEvent) { sidebar.getChildren().setAll(getActionPromptView(), navigationPane); backButton.setVisible(false); playButton.setText("Stop"); ImageView buttonGraphic = (ImageView) playButton.getGraphic(); buttonGraphic.setImage(new Image(IconFactory.getImageUrl("ui/pause_icon.png"))); playButton.setOnAction(this::stopPlayMode); NotificationProxy.sendNotification(GameNotification.START_PLAY_SANDBOX); } private void stopPlayMode(ActionEvent actionEvent) { sidebar.getChildren().setAll(toolboxView, navigationPane); backButton.setVisible(true); playButton.setText("Play"); ImageView buttonGraphic = (ImageView) playButton.getGraphic(); buttonGraphic.setImage(new Image(IconFactory.getImageUrl("ui/play_icon.png"))); playButton.setOnAction(this::startPlayMode); NotificationProxy.sendNotification(GameNotification.STOP_PLAY_SANDBOX); } public void updateSandbox(GameContext context) { if (firstUpdate) { setCenter(getBoardView()); firstUpdate = false; } getBoardView().updateGameState(context); if (toolboxView.getParent() != null) { toolboxView.setContext(context); } } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/SandboxProxy.java ================================================ package net.demilich.metastone.gui.sandboxmode; import net.demilich.nittygrittymvc.Proxy; import net.demilich.metastone.GameNotification; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.Player; public class SandboxProxy extends Proxy { public static final String NAME = "SandboxProxy"; private GameContext sandbox; private Player selectedPlayer; public SandboxProxy() { super(NAME); } public GameContext getSandbox() { return sandbox; } public Player getSelectedPlayer() { return selectedPlayer; } public void setSandbox(GameContext sandbox) { this.sandbox = sandbox; } public void setSelectedPlayer(Player selectedPlayer) { this.selectedPlayer = selectedPlayer; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/ToolboxView.java ================================================ package net.demilich.metastone.gui.sandboxmode; import java.io.IOException; import javafx.fxml.FXMLLoader; import javafx.scene.control.Separator; import javafx.scene.control.ToolBar; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.Player; public class ToolboxView extends ToolBar { private final PlayerPanel playerPanel; private final CardPanel cardPanel; private final MinionPanel minionPanel; public ToolboxView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/ToolboxView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } playerPanel = new PlayerPanel(); getItems().add(playerPanel); getItems().add(new Separator()); cardPanel = new CardPanel(); getItems().add(cardPanel); getItems().add(new Separator()); minionPanel = new MinionPanel(); getItems().add(minionPanel); } public void onPlayerSelectionChanged(Player selectedPlayer) { cardPanel.onPlayerSelectionChanged(selectedPlayer); } public void setContext(GameContext context) { playerPanel.setContext(context); minionPanel.setContext(context); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/actions/EditEntityAction.java ================================================ package net.demilich.metastone.gui.sandboxmode.actions; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.actions.ActionType; import net.demilich.metastone.game.actions.GameAction; import net.demilich.metastone.game.entities.Entity; import net.demilich.metastone.game.targeting.TargetSelection; import net.demilich.metastone.gui.sandboxmode.EntityEditor; public class EditEntityAction extends GameAction { public EditEntityAction() { setTargetRequirement(TargetSelection.ANY); setActionType(ActionType.SYSTEM); } @Override public void execute(GameContext context, int playerId) { Entity entity = context.resolveSingleTarget(getTargetKey()); EntityEditor editor = new EntityEditor(entity, result -> NotificationProxy.sendNotification(GameNotification.UPDATE_SANDBOX_STATE, context)); NotificationProxy.sendNotification(GameNotification.SHOW_MODAL_DIALOG, editor); } @Override public String getPromptText() { return "[Edit entity]"; } @Override public boolean isSameActionGroup(GameAction anotherAction) { return false; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/actions/KillAction.java ================================================ package net.demilich.metastone.gui.sandboxmode.actions; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.actions.ActionType; import net.demilich.metastone.game.actions.GameAction; import net.demilich.metastone.game.entities.Actor; import net.demilich.metastone.game.targeting.TargetSelection; public class KillAction extends GameAction { public KillAction() { setTargetRequirement(TargetSelection.MINIONS); setActionType(ActionType.SYSTEM); } @Override public void execute(GameContext context, int playerId) { Actor target = (Actor) context.resolveSingleTarget(getTargetKey()); context.getLogic().markAsDestroyed(target); } @Override public String getPromptText() { return "[Kill]"; } @Override public boolean isSameActionGroup(GameAction anotherAction) { return anotherAction instanceof KillAction; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/actions/SetManaAction.java ================================================ package net.demilich.metastone.gui.sandboxmode.actions; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.Player; import net.demilich.metastone.game.actions.GameAction; import net.demilich.metastone.game.targeting.TargetSelection; public class SetManaAction extends GameAction { private final int targetPlayerId; private final int mana; public SetManaAction(int playerId, int mana) { this.targetPlayerId = playerId; this.mana = mana; setTargetRequirement(TargetSelection.NONE); } @Override public void execute(GameContext context, int playerId) { Player player = context.getPlayer(targetPlayerId); player.setMana(mana); } @Override public String getPromptText() { return "[SetMana]"; } @Override public boolean isSameActionGroup(GameAction anotherAction) { return false; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/actions/SetMaxManaAction.java ================================================ package net.demilich.metastone.gui.sandboxmode.actions; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.Player; import net.demilich.metastone.game.actions.GameAction; import net.demilich.metastone.game.targeting.TargetSelection; public class SetMaxManaAction extends GameAction { private final int targetPlayerId; private final int mana; public SetMaxManaAction(int playerId, int mana) { this.targetPlayerId = playerId; this.mana = mana; setTargetRequirement(TargetSelection.NONE); } @Override public void execute(GameContext context, int playerId) { Player player = context.getPlayer(targetPlayerId); player.setMaxMana(mana); } @Override public String getPromptText() { return "[SetMana]"; } @Override public boolean isSameActionGroup(GameAction anotherAction) { return false; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/actions/SilenceAction.java ================================================ package net.demilich.metastone.gui.sandboxmode.actions; import net.demilich.metastone.game.GameContext; import net.demilich.metastone.game.actions.ActionType; import net.demilich.metastone.game.actions.GameAction; import net.demilich.metastone.game.entities.minions.Minion; import net.demilich.metastone.game.targeting.TargetSelection; public class SilenceAction extends GameAction { public SilenceAction() { setTargetRequirement(TargetSelection.MINIONS); setActionType(ActionType.SYSTEM); } @Override public void execute(GameContext context, int playerId) { Minion target = (Minion) context.resolveSingleTarget(getTargetKey()); context.getLogic().silence(playerId, target); } @Override public String getPromptText() { return "[Silence]"; } @Override public boolean isSameActionGroup(GameAction anotherAction) { return anotherAction instanceof SilenceAction; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/commands/CreateNewSandboxCommand.java ================================================ package net.demilich.metastone.gui.sandboxmode.commands; 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.DoNothingBehaviour; import net.demilich.metastone.game.decks.DeckFormat; import net.demilich.metastone.game.logic.GameLogic; import net.demilich.metastone.game.gameconfig.GameConfig; import net.demilich.metastone.game.gameconfig.PlayerConfig; import net.demilich.metastone.gui.playmode.GameContextVisualizable; import net.demilich.metastone.gui.sandboxmode.SandboxProxy; public class CreateNewSandboxCommand extends SimpleCommand { @Override public void execute(INotification notification) { Thread thread = new Thread(new Runnable() { @Override public void run() { GameConfig gameConfig = (GameConfig) notification.getBody(); SandboxProxy sandboxProxy = (SandboxProxy) getFacade().retrieveProxy(SandboxProxy.NAME); PlayerConfig player1Config = gameConfig.getPlayerConfig1(); player1Config.setName("Player 1"); Player player1 = new Player(player1Config); player1.setBehaviour(new DoNothingBehaviour()); PlayerConfig player2Config = gameConfig.getPlayerConfig2(); player2Config.setName("Player 2"); Player player2 = new Player(player2Config); player2.setBehaviour(new DoNothingBehaviour()); DeckFormat deckFormat = gameConfig.getDeckFormat(); GameContext sandbox = new GameContextVisualizable(player1, player2, new GameLogic(), deckFormat); sandboxProxy.setSandbox(sandbox); sendNotification(GameNotification.UPDATE_SANDBOX_STATE, sandbox); player1.setBehaviour(player1Config.getBehaviour()); player2.setBehaviour(player2Config.getBehaviour()); sandbox.setIgnoreEvents(true); sandbox.play(); } }); thread.setDaemon(true); thread.setUncaughtExceptionHandler((t, exception) -> exception.printStackTrace()); thread.start(); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/commands/ModifyPlayerDeckCommand.java ================================================ package net.demilich.metastone.gui.sandboxmode.commands; 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.cards.Card; import net.demilich.metastone.game.cards.CardCollection; import net.demilich.metastone.gui.sandboxmode.SandboxProxy; public class ModifyPlayerDeckCommand extends SimpleCommand { @Override public void execute(INotification notification) { SandboxProxy sandboxProxy = (SandboxProxy) getFacade().retrieveProxy(SandboxProxy.NAME); Player player = sandboxProxy.getSelectedPlayer(); CardCollection modifiedDeck = (CardCollection) notification.getBody(); GameContext context = sandboxProxy.getSandbox(); for (Card card : player.getDeck().toList()) { context.getLogic().removeCardFromDeck(player.getId(), card); } for (Card card : modifiedDeck) { context.getLogic().shuffleToDeck(player, card); } sendNotification(GameNotification.UPDATE_SANDBOX_STATE, sandboxProxy.getSandbox()); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/commands/ModifyPlayerHandCommand.java ================================================ package net.demilich.metastone.gui.sandboxmode.commands; 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.cards.Card; import net.demilich.metastone.game.cards.CardCollection; import net.demilich.metastone.gui.sandboxmode.SandboxProxy; public class ModifyPlayerHandCommand extends SimpleCommand { @Override public void execute(INotification notification) { SandboxProxy sandboxProxy = (SandboxProxy) getFacade().retrieveProxy(SandboxProxy.NAME); Player player = sandboxProxy.getSelectedPlayer(); CardCollection modifiedHand = (CardCollection) notification.getBody(); GameContext context = sandboxProxy.getSandbox(); for (Card card : player.getHand().toList()) { context.getLogic().removeCard(player.getId(), card); } for (Card card : modifiedHand) { context.getLogic().receiveCard(player.getId(), card); } sendNotification(GameNotification.UPDATE_SANDBOX_STATE, sandboxProxy.getSandbox()); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/commands/PerformActionCommand.java ================================================ package net.demilich.metastone.gui.sandboxmode.commands; import java.util.ArrayList; import java.util.List; 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.actions.GameAction; import net.demilich.metastone.game.behaviour.human.ActionGroup; import net.demilich.metastone.game.behaviour.human.HumanTargetOptions; import net.demilich.metastone.game.logic.ActionLogic; import net.demilich.metastone.game.targeting.TargetSelection; import net.demilich.metastone.gui.sandboxmode.SandboxProxy; public class PerformActionCommand extends SimpleCommand { @Override public void execute(INotification notification) { SandboxProxy sandboxProxy = (SandboxProxy) getFacade().retrieveProxy(SandboxProxy.NAME); GameAction gameAction = (GameAction) notification.getBody(); ActionLogic actionLogic = new ActionLogic(); GameContext context = sandboxProxy.getSandbox(); Player selectedPlayer = sandboxProxy.getSelectedPlayer(); List rolledOutActions = new ArrayList(); actionLogic.rollout(gameAction, context, selectedPlayer, rolledOutActions); if (rolledOutActions.isEmpty()) { return; } if (gameAction.getTargetRequirement() != TargetSelection.NONE && gameAction.getTargetRequirement() != TargetSelection.AUTO) { ActionGroup actionGroup = new ActionGroup(rolledOutActions.get(0)); for (GameAction rolledAction : rolledOutActions) { actionGroup.add(rolledAction); } HumanTargetOptions targetOptions = new HumanTargetOptions(this::performAction, context, selectedPlayer.getId(), actionGroup); sendNotification(GameNotification.SELECT_TARGET, targetOptions); } else { performAction(gameAction); } } private void performAction(GameAction action) { SandboxProxy sandboxProxy = (SandboxProxy) getFacade().retrieveProxy(SandboxProxy.NAME); GameContext context = sandboxProxy.getSandbox(); Player selectedPlayer = sandboxProxy.getSelectedPlayer(); action.setSource(selectedPlayer.getReference()); context.getLogic().performGameAction(selectedPlayer.getId(), action); sendNotification(GameNotification.UPDATE_SANDBOX_STATE, context); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/commands/SelectPlayerCommand.java ================================================ package net.demilich.metastone.gui.sandboxmode.commands; import net.demilich.nittygrittymvc.SimpleCommand; import net.demilich.nittygrittymvc.interfaces.INotification; import net.demilich.metastone.GameNotification; import net.demilich.metastone.game.Player; import net.demilich.metastone.gui.sandboxmode.SandboxProxy; public class SelectPlayerCommand extends SimpleCommand { @Override public void execute(INotification notification) { Player player = (Player) notification.getBody(); SandboxProxy sandboxProxy = (SandboxProxy) getFacade().retrieveProxy(SandboxProxy.NAME); sandboxProxy.setSelectedPlayer(player); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/commands/SpawnMinionCommand.java ================================================ package net.demilich.metastone.gui.sandboxmode.commands; import java.util.ArrayList; import java.util.List; 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.actions.GameAction; import net.demilich.metastone.game.behaviour.human.ActionGroup; import net.demilich.metastone.game.behaviour.human.HumanTargetOptions; import net.demilich.metastone.game.cards.MinionCard; import net.demilich.metastone.game.entities.Actor; import net.demilich.metastone.game.entities.minions.Minion; import net.demilich.metastone.game.events.BoardChangedEvent; import net.demilich.metastone.game.logic.ActionLogic; import net.demilich.metastone.gui.sandboxmode.SandboxProxy; public class SpawnMinionCommand extends SimpleCommand { private MinionCard minionCard; @Override public void execute(INotification notification) { minionCard = (MinionCard) notification.getBody(); SandboxProxy sandboxProxy = (SandboxProxy) getFacade().retrieveProxy(SandboxProxy.NAME); GameAction summonAction = minionCard.play(); ActionLogic actionLogic = new ActionLogic(); GameContext context = sandboxProxy.getSandbox(); Player selectedPlayer = sandboxProxy.getSelectedPlayer(); List rolledOutActions = new ArrayList(); actionLogic.rollout(summonAction, context, selectedPlayer, rolledOutActions); ActionGroup actionGroup = new ActionGroup(summonAction); for (GameAction gameAction : rolledOutActions) { actionGroup.add(gameAction); } HumanTargetOptions targetOptions = new HumanTargetOptions(this::spawnMinion, context, selectedPlayer.getId(), actionGroup); sendNotification(GameNotification.SELECT_TARGET, targetOptions); } private void spawnMinion(GameAction action) { SandboxProxy sandboxProxy = (SandboxProxy) getFacade().retrieveProxy(SandboxProxy.NAME); GameContext context = sandboxProxy.getSandbox(); Player selectedPlayer = sandboxProxy.getSelectedPlayer(); Minion minion = minionCard.summon(); Actor nextTo = (Actor) context.resolveSingleTarget(action.getTargetKey()); int index = selectedPlayer.getSummons().indexOf(nextTo); context.getLogic().summon(selectedPlayer.getId(), minion, minionCard, index, false); if (context.ignoreEvents()) { context.setIgnoreEvents(false); context.fireGameEvent(new BoardChangedEvent(context)); context.setIgnoreEvents(true); } sendNotification(GameNotification.UPDATE_SANDBOX_STATE, context); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/commands/StartPlaySandboxCommand.java ================================================ package net.demilich.metastone.gui.sandboxmode.commands; 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.gui.sandboxmode.SandboxProxy; public class StartPlaySandboxCommand extends SimpleCommand { @Override public void execute(INotification notification) { SandboxProxy sandboxProxy = (SandboxProxy) getFacade().retrieveProxy(SandboxProxy.NAME); GameContext context = sandboxProxy.getSandbox(); context.setIgnoreEvents(false); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/sandboxmode/commands/StopPlaySandboxCommand.java ================================================ package net.demilich.metastone.gui.sandboxmode.commands; 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.gui.sandboxmode.SandboxProxy; public class StopPlaySandboxCommand extends SimpleCommand { @Override public void execute(INotification notification) { SandboxProxy sandboxProxy = (SandboxProxy) getFacade().retrieveProxy(SandboxProxy.NAME); GameContext context = sandboxProxy.getSandbox(); context.setIgnoreEvents(true); getFacade().sendNotification(GameNotification.UPDATE_SANDBOX_STATE, context); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/simulationmode/PlayerConfigView.java ================================================ package net.demilich.metastone.gui.simulationmode; import net.demilich.metastone.gui.playmode.config.PlayerConfigType; public class PlayerConfigView extends net.demilich.metastone.gui.gameconfig.PlayerConfigView { public PlayerConfigView() { super(PlayerConfigType.SIMULATION); setPrefHeight(400); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/simulationmode/PlayerInfoView.java ================================================ package net.demilich.metastone.gui.simulationmode; import java.io.IOException; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Label; import javafx.scene.image.ImageView; import javafx.scene.layout.Pane; import net.demilich.metastone.game.cards.HeroCard; import net.demilich.metastone.gui.IconFactory; import net.demilich.metastone.game.gameconfig.PlayerConfig; public class PlayerInfoView extends Pane { @FXML private ImageView classIcon; @FXML private Label heroLabel; @FXML private Label deckLabel; @FXML private Label behaviourLabel; public PlayerInfoView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/PlayerInfoView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } } public void setInfo(PlayerConfig playerConfig) { HeroCard heroCard = playerConfig.getHeroCard(); classIcon.setImage(IconFactory.getClassIcon(heroCard.getHeroClass())); heroLabel.setText(playerConfig.getName()); deckLabel.setText(playerConfig.getDeck().getName()); behaviourLabel.setText(playerConfig.getBehaviour().getName()); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/simulationmode/SimulateGamesCommand.java ================================================ package net.demilich.metastone.gui.simulationmode; import java.util.ArrayList; 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.Notification; 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.decks.DeckFormat; import net.demilich.metastone.game.logic.GameLogic; import net.demilich.metastone.game.gameconfig.GameConfig; import net.demilich.metastone.game.gameconfig.PlayerConfig; import net.demilich.metastone.utils.Tuple; public class SimulateGamesCommand extends SimpleCommand { private class PlayGameTask implements Callable { private final GameConfig gameConfig; public PlayGameTask(GameConfig gameConfig) { this.gameConfig = gameConfig; } @Override public Void call() throws Exception { PlayerConfig playerConfig1 = gameConfig.getPlayerConfig1(); PlayerConfig playerConfig2 = gameConfig.getPlayerConfig2(); Player player1 = new Player(playerConfig1); Player player2 = new Player(playerConfig2); DeckFormat deckFormat = gameConfig.getDeckFormat(); GameContext newGame = new GameContext(player1, player2, new GameLogic(), deckFormat); newGame.play(); onGameComplete(gameConfig, newGame); newGame.dispose(); return null; } } private static Logger logger = LoggerFactory.getLogger(SimulateGamesCommand.class); private int gamesCompleted; private long lastUpdate; private SimulationResult result; @Override public void execute(INotification notification) { final GameConfig gameConfig = (GameConfig) notification.getBody(); result = new SimulationResult(gameConfig); gamesCompleted = 0; Thread t = new Thread(new Runnable() { @Override public void run() { int cores = Runtime.getRuntime().availableProcessors(); logger.info("Starting simulation on " + cores + " cores"); ExecutorService executor = Executors.newFixedThreadPool(cores); // ExecutorService executor = // Executors.newSingleThreadExecutor(); List> futures = new ArrayList>(); // send initial status update Tuple progress = new Tuple<>(0, gameConfig.getNumberOfGames()); getFacade().sendNotification(GameNotification.SIMULATION_PROGRESS_UPDATE, progress); // queue up all games as tasks lastUpdate = System.currentTimeMillis(); for (int i = 0; i < gameConfig.getNumberOfGames(); i++) { PlayGameTask task = new PlayGameTask(gameConfig); 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(50); } catch (InterruptedException e) { e.printStackTrace(); } } result.calculateMetaStatistics(); getFacade().sendNotification(GameNotification.SIMULATION_RESULT, result); logger.info("Simulation finished"); } }); t.setDaemon(true); t.start(); } private void onGameComplete(GameConfig gameConfig, GameContext context) { long timeStamp = System.currentTimeMillis(); gamesCompleted++; if (timeStamp - lastUpdate > 100) { lastUpdate = timeStamp; Tuple progress = new Tuple<>(gamesCompleted, gameConfig.getNumberOfGames()); Notification updateNotification = new Notification<>(GameNotification.SIMULATION_PROGRESS_UPDATE, progress); getFacade().notifyObservers(updateNotification); } synchronized (result) { result.getPlayer1Stats().merge(context.getPlayer1().getStatistics()); result.getPlayer2Stats().merge(context.getPlayer2().getStatistics()); } } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/simulationmode/SimulationMediator.java ================================================ package net.demilich.metastone.gui.simulationmode; 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.application.Platform; import net.demilich.metastone.GameNotification; import net.demilich.metastone.game.decks.Deck; import net.demilich.metastone.game.decks.DeckFormat; import net.demilich.metastone.utils.Tuple; public class SimulationMediator extends Mediator { public static final String NAME = "SimulationMediator"; private static Logger logger = LoggerFactory.getLogger(SimulationMediator.class); private final SimulationModeConfigView view; private final WaitForSimulationView waitView; private final SimulationResultView resultView; public SimulationMediator() { super(NAME); view = new SimulationModeConfigView(); waitView = new WaitForSimulationView(); resultView = new SimulationResultView(); } @Override @SuppressWarnings("unchecked") public void handleNotification(final INotification notification) { switch (notification.getId()) { case REPLY_DECKS: List decks = (List) notification.getBody(); view.injectDecks(decks); break; case REPLY_DECK_FORMATS: List deckFormats = (List) notification.getBody(); view.injectDeckFormats(deckFormats); break; case COMMIT_SIMULATIONMODE_CONFIG: getFacade().sendNotification(GameNotification.SHOW_MODAL_DIALOG, waitView); getFacade().sendNotification(GameNotification.SIMULATE_GAMES, notification.getBody()); break; case SIMULATION_PROGRESS_UPDATE: Tuple progress = (Tuple) notification.getBody(); Platform.runLater(new Runnable() { @Override public void run() { waitView.update(progress.getFirst(), progress.getSecond()); } }); break; case SIMULATION_RESULT: Platform.runLater(new Runnable() { @Override public void run() { waitView.getScene().getWindow().hide(); SimulationResult result = (SimulationResult) notification.getBody(); resultView.showSimulationResult(result); getFacade().sendNotification(GameNotification.SHOW_VIEW, resultView); } }); break; default: logger.warn("Unhandled notification {} in {}", notification, getClass().getSimpleName()); break; } } @Override public List listNotificationInterests() { List notificationInterests = new ArrayList(); notificationInterests.add(GameNotification.REPLY_DECKS); notificationInterests.add(GameNotification.REPLY_DECK_FORMATS); notificationInterests.add(GameNotification.COMMIT_SIMULATIONMODE_CONFIG); notificationInterests.add(GameNotification.SIMULATION_PROGRESS_UPDATE); notificationInterests.add(GameNotification.SIMULATION_RESULT); return notificationInterests; } @Override public void onRegister() { getFacade().sendNotification(GameNotification.SHOW_VIEW, view); getFacade().sendNotification(GameNotification.REQUEST_DECKS); getFacade().sendNotification(GameNotification.REQUEST_DECK_FORMATS); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/simulationmode/SimulationModeConfigView.java ================================================ package net.demilich.metastone.gui.simulationmode; import java.io.IOException; import java.util.List; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; 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.ComboBox; import javafx.scene.layout.BorderPane; 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.game.decks.DeckFormat; import net.demilich.metastone.gui.common.DeckFormatStringConverter; import net.demilich.metastone.game.gameconfig.GameConfig; public class SimulationModeConfigView extends BorderPane implements EventHandler { @FXML protected ComboBox formatBox; @FXML protected HBox playerArea; @FXML protected Button startButton; @FXML protected Button backButton; @FXML protected ComboBox numberOfGamesBox; protected PlayerConfigView player1Config; protected PlayerConfigView player2Config; private List deckFormats; public SimulationModeConfigView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/SimulationModeConfigView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } formatBox.setConverter(new DeckFormatStringConverter()); player1Config = new PlayerConfigView(); player2Config = new PlayerConfigView(); playerArea.getChildren().add(player1Config); playerArea.getChildren().add(player2Config); startButton.setOnAction(this); backButton.setOnAction(this); setupNumberOfGamesBox(); formatBox.valueProperty().addListener((ChangeListener) (observableProperty, oldDeckFormat, newDeckFormat) -> { setDeckFormats(newDeckFormat); }); } private void setupDeckFormats() { ObservableList deckFormatList = FXCollections.observableArrayList(); for (DeckFormat deckFormat : deckFormats) { deckFormatList.add(deckFormat); } formatBox.setItems(deckFormatList); formatBox.getSelectionModel().selectFirst(); } private void setDeckFormats(DeckFormat newDeckFormat) { player1Config.setDeckFormat(newDeckFormat); player2Config.setDeckFormat(newDeckFormat); } @Override public void handle(ActionEvent actionEvent) { if (actionEvent.getSource() == startButton) { GameConfig gameConfig = new GameConfig(); gameConfig.setNumberOfGames(numberOfGamesBox.getSelectionModel().getSelectedItem()); gameConfig.setPlayerConfig1(player1Config.getPlayerConfig()); gameConfig.setPlayerConfig2(player2Config.getPlayerConfig()); gameConfig.setDeckFormat(formatBox.getValue()); NotificationProxy.sendNotification(GameNotification.COMMIT_SIMULATIONMODE_CONFIG, gameConfig); } else if (actionEvent.getSource() == backButton) { NotificationProxy.sendNotification(GameNotification.MAIN_MENU); } } public void injectDecks(List decks) { player1Config.injectDecks(decks); player2Config.injectDecks(decks); } public void injectDeckFormats(List deckFormats) { this.deckFormats = deckFormats; setupDeckFormats(); player1Config.setDeckFormat(formatBox.getValue()); player2Config.setDeckFormat(formatBox.getValue()); } private void setupNumberOfGamesBox() { ObservableList numberOfGamesEntries = FXCollections.observableArrayList(); numberOfGamesEntries.add(1); numberOfGamesEntries.add(10); numberOfGamesEntries.add(100); numberOfGamesEntries.add(1000); numberOfGamesEntries.add(10000); numberOfGamesEntries.add(100000); numberOfGamesBox.setItems(numberOfGamesEntries); numberOfGamesBox.getSelectionModel().select(2); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/simulationmode/SimulationResult.java ================================================ package net.demilich.metastone.gui.simulationmode; import net.demilich.metastone.game.statistics.GameStatistics; import net.demilich.metastone.game.statistics.Statistic; import net.demilich.metastone.game.gameconfig.GameConfig; public class SimulationResult { private final GameStatistics player1Stats = new GameStatistics(); private final GameStatistics player2Stats = new GameStatistics(); private final long startTimestamp; private long duration; private final GameConfig config; public SimulationResult(GameConfig config) { this.config = config; this.startTimestamp = System.currentTimeMillis(); } public void calculateMetaStatistics() { calculateMetaStatistics(player1Stats); calculateMetaStatistics(player2Stats); } private void calculateMetaStatistics(GameStatistics statistics) { double gamesPlayed = getNumberOfGames(); double winRate = statistics.getLong(Statistic.GAMES_WON) / gamesPlayed * 100; String winRateString = String.format("%.2f", winRate) + "%"; statistics.set(Statistic.WIN_RATE, winRateString); long endTimestamp = System.currentTimeMillis(); duration = endTimestamp - startTimestamp; } public GameConfig getConfig() { return config; } public long getDuration() { return this.duration; } public int getNumberOfGames() { return getConfig().getNumberOfGames(); } public GameStatistics getPlayer1Stats() { return player1Stats; } public GameStatistics getPlayer2Stats() { return player2Stats; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/simulationmode/SimulationResultView.java ================================================ package net.demilich.metastone.gui.simulationmode; import java.io.IOException; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import net.demilich.metastone.NotificationProxy; import org.apache.commons.lang3.time.DurationFormatUtils; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.BorderPane; 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.CardType; import net.demilich.metastone.game.logic.GameLogic; import net.demilich.metastone.game.statistics.GameStatistics; import net.demilich.metastone.game.statistics.Statistic; public class SimulationResultView extends BorderPane { private static String getStatName(Statistic stat) { switch (stat) { case ARMOR_GAINED: return "Armor gained"; case CARDS_DRAWN: return "Cards drawn"; case CARDS_PLAYED: return "Cards played"; case DAMAGE_DEALT: return "Damage dealt"; case FATIGUE_DAMAGE: return "Fatigue damage"; case GAMES_LOST: return "Games Lost"; case GAMES_WON: return "Games won"; case HEALING_DONE: return "Healing done"; case HERO_POWER_USED: return "Hero power used"; case MANA_SPENT: return "Mana spent"; case MINIONS_PLAYED: return "Minions played"; case SPELLS_CAST: return "Spells cast"; case TURNS_TAKEN: return "Turns taken"; case WEAPONS_EQUIPPED: return "Weapons equipped"; case WIN_RATE: return "Win rate"; default: break; } return stat.toString(); } @FXML private BorderPane infoArea; @FXML private TableView absoluteResultTable; @FXML private TableView averageResultTable; @FXML private Button doneButton; @FXML private Label durationLabel; private PlayerInfoView player1InfoView; private PlayerInfoView player2InfoView; private final NumberFormat formatter = DecimalFormat.getInstance(); public SimulationResultView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/SimulationResultView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } doneButton.setOnAction(event -> NotificationProxy.sendNotification(GameNotification.MAIN_MENU)); player1InfoView = new PlayerInfoView(); infoArea.setLeft(player1InfoView); player2InfoView = new PlayerInfoView(); infoArea.setRight(player2InfoView); formatter.setMinimumFractionDigits(0); formatter.setMaximumFractionDigits(2); } private String getAverageStatString(Statistic stat, GameStatistics playerStatistics, int numberOfGames) { if (playerStatistics.contains(stat)) { Object statValue = playerStatistics.get(stat); if (statValue instanceof Number) { double value = ((Number) statValue).doubleValue(); return formatter.format(value / numberOfGames); } } return "-"; } private String getFavouriteCardName(GameStatistics stats, CardType cardType) { List cards = new ArrayList(); for (String cardId : stats.getCardsPlayed().keySet()) { if (cardId.startsWith(GameLogic.TEMP_CARD_LABEL)) { continue; } Card card = CardCatalogue.getCardById(cardId); if (card == null) { System.out.println("Invalid card with id: " + cardId); } if (card.getCardType() == cardType) { cards.add(card); } } if (cards.isEmpty()) { return "-"; } Collections.sort(cards, (c1, c2) -> { int c1Count = stats.getCardsPlayedCount(c1.getCardId()); int c2Count = stats.getCardsPlayedCount(c2.getCardId()); // sort descending return Integer.compare(c2Count, c1Count); }); return cards.get(0).getName(); } private String getStatString(Statistic stat, GameStatistics playerStatistics) { if (playerStatistics.contains(stat)) { Object statValue = playerStatistics.get(stat); if (statValue instanceof Number) { return formatter.format(playerStatistics.get(stat)); } return statValue.toString(); } return "-"; } @SuppressWarnings({ "rawtypes", "unchecked" }) public void showSimulationResult(SimulationResult result) { player1InfoView.setInfo(result.getConfig().getPlayerConfig1()); player2InfoView.setInfo(result.getConfig().getPlayerConfig2()); durationLabel.setText("Simulation took " + DurationFormatUtils.formatDurationHMS(result.getDuration())); ObservableList absoluteStatEntries = FXCollections.observableArrayList(); ObservableList averageStatEntries = FXCollections.observableArrayList(); for (Statistic stat : Statistic.values()) { StatEntry absoluteStatEntry = new StatEntry(); absoluteStatEntry.setStatName(getStatName(stat)); absoluteStatEntry.setPlayer1Value(getStatString(stat, result.getPlayer1Stats())); absoluteStatEntry.setPlayer2Value(getStatString(stat, result.getPlayer2Stats())); absoluteStatEntries.add(absoluteStatEntry); StatEntry averageStatEntry = new StatEntry(); averageStatEntry.setStatName(getStatName(stat)); averageStatEntry.setPlayer1Value(getAverageStatString(stat, result.getPlayer1Stats(), result.getNumberOfGames())); averageStatEntry.setPlayer2Value(getAverageStatString(stat, result.getPlayer2Stats(), result.getNumberOfGames())); averageStatEntries.add(averageStatEntry); } StatEntry favouriteMinionCard = new StatEntry(); favouriteMinionCard.setStatName("Favourite minion card"); favouriteMinionCard.setPlayer1Value(getFavouriteCardName(result.getPlayer1Stats(), CardType.MINION)); favouriteMinionCard.setPlayer2Value(getFavouriteCardName(result.getPlayer2Stats(), CardType.MINION)); absoluteStatEntries.add(favouriteMinionCard); StatEntry favouriteSpellCard = new StatEntry(); favouriteSpellCard.setStatName("Favourite spell card"); favouriteSpellCard.setPlayer1Value(getFavouriteCardName(result.getPlayer1Stats(), CardType.SPELL)); favouriteSpellCard.setPlayer2Value(getFavouriteCardName(result.getPlayer2Stats(), CardType.SPELL)); absoluteStatEntries.add(favouriteSpellCard); StatEntry favouriteWeaponCard = new StatEntry(); favouriteWeaponCard.setStatName("Favourite weapon card"); favouriteWeaponCard.setPlayer1Value(getFavouriteCardName(result.getPlayer1Stats(), CardType.WEAPON)); favouriteWeaponCard.setPlayer2Value(getFavouriteCardName(result.getPlayer2Stats(), CardType.WEAPON)); absoluteStatEntries.add(favouriteWeaponCard); absoluteResultTable.setItems(absoluteStatEntries); absoluteResultTable.getColumns().get(0).setCellValueFactory(new PropertyValueFactory("statName")); absoluteResultTable.getColumns().get(1).setCellValueFactory(new PropertyValueFactory("player1Value")); absoluteResultTable.getColumns().get(2).setCellValueFactory(new PropertyValueFactory("player2Value")); averageResultTable.setItems(averageStatEntries); averageResultTable.getColumns().get(0).setCellValueFactory(new PropertyValueFactory("statName")); averageResultTable.getColumns().get(1).setCellValueFactory(new PropertyValueFactory("player1Value")); averageResultTable.getColumns().get(2).setCellValueFactory(new PropertyValueFactory("player2Value")); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/simulationmode/StatEntry.java ================================================ package net.demilich.metastone.gui.simulationmode; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; public class StatEntry { private final StringProperty statName = new SimpleStringProperty(this, "statName"); private final StringProperty player1Value = new SimpleStringProperty(this, "player1Value"); private final StringProperty player2Value = new SimpleStringProperty(this, "player2Value"); public StatEntry() { } public StatEntry(String statName, String player1Value, String player2Value) { setStatName(statName); setPlayer1Value(player1Value); setPlayer2Value(player2Value); } public String getPlayer1Value() { return player1Value.get(); } public String getPlayer2Value() { return player2Value.get(); } public String getStatName() { return statName.get(); } public StringProperty player1ValueProperty() { return player1Value; } public StringProperty player2ValueProperty() { return player2Value; } public void setPlayer1Value(String value) { player1Value.set(value); } public void setPlayer2Value(String value) { player2Value.set(value); } public void setStatName(String value) { statName.set(value); } public StringProperty statNameProperty() { return statName; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/simulationmode/WaitForSimulationView.java ================================================ package net.demilich.metastone.gui.simulationmode; import java.io.IOException; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.layout.BorderPane; public class WaitForSimulationView extends BorderPane { @FXML private ProgressBar progressBar; @FXML private Label gamesCompletedLabel; public WaitForSimulationView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/WaitForSimulationView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } } public void update(int gamesCompleted, int gamesTotal) { gamesCompletedLabel.setText(gamesCompleted + "/" + gamesTotal + " games completed"); progressBar.setProgress(gamesCompleted / (double) gamesTotal); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/trainingmode/PerformTrainingCommand.java ================================================ package net.demilich.metastone.gui.trainingmode; import net.demilich.metastone.trainingmode.TrainingData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.demilich.nittygrittymvc.Notification; 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.threat.FeatureVector; import net.demilich.metastone.game.behaviour.threat.GameStateValueBehaviour; import net.demilich.metastone.game.behaviour.threat.cuckoo.CuckooLearner; import net.demilich.metastone.game.cards.CardSet; import net.demilich.metastone.game.decks.DeckFormat; import net.demilich.metastone.game.logic.GameLogic; import net.demilich.metastone.game.statistics.Statistic; import net.demilich.metastone.game.gameconfig.PlayerConfig; public class PerformTrainingCommand extends SimpleCommand { private static Logger logger = LoggerFactory.getLogger(PerformTrainingCommand.class); private int gamesCompleted; private int gamesWon; @Override public void execute(INotification notification) { final TrainingConfig config = (TrainingConfig) notification.getBody(); if (config.getDecks().isEmpty()) { logger.error("Deck list is empty!!"); } gamesCompleted = 0; gamesWon = 0; Thread t = new Thread(new Runnable() { @Override public void run() { logger.info("Training started"); CuckooLearner learner = new CuckooLearner(config.getDeckToTrain(), config.getDecks()); // send initial status update TrainingProgressReport progress = new TrainingProgressReport(gamesCompleted, config.getNumberOfGames(), gamesWon); getFacade().sendNotification(GameNotification.TRAINING_PROGRESS_UPDATE, progress); for (int i = 0; i < config.getNumberOfGames(); i++) { FeatureVector fittest = learner.getFittest(); PlayerConfig learnerConfig = new PlayerConfig(config.getDeckToTrain(), new GameStateValueBehaviour(fittest, "(fittest)")); learnerConfig.setName("Learner"); Player player1 = new Player(learnerConfig); PlayerConfig opponentConfig = new PlayerConfig(config.getRandomDeck(), new GameStateValueBehaviour()); opponentConfig.setName("Opponent"); Player player2 = new Player(opponentConfig); DeckFormat deckFormat = new DeckFormat(); for (CardSet set : CardSet.values()) { deckFormat.addSet(set); } GameContext newGame = new GameContext(player1, player2, new GameLogic(), deckFormat); newGame.play(); onGameComplete(config, newGame); newGame.dispose(); if (i % 10 == 0) { learner.evolve(); } } getFacade().sendNotification(GameNotification.TRAINING_PROGRESS_UPDATE, new TrainingProgressReport(gamesCompleted, config.getNumberOfGames(), gamesWon)); logger.info("Training ended"); FeatureVector fittest = learner.getFittest(); logger.info("**************Final weights: {}", fittest); // save training data getFacade().sendNotification(GameNotification.SAVE_TRAINING_DATA, new TrainingData(config.getDeckToTrain().getName(), fittest)); } }); t.setDaemon(true); t.start(); } private void onGameComplete(TrainingConfig config, GameContext completedGame) { gamesCompleted++; gamesWon += completedGame.getPlayer1().getStatistics().getLong(Statistic.GAMES_WON); TrainingProgressReport progress = new TrainingProgressReport(gamesCompleted, config.getNumberOfGames(), gamesWon); Notification updateNotification = new Notification<>(GameNotification.TRAINING_PROGRESS_UPDATE, progress); getFacade().notifyObservers(updateNotification); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/trainingmode/RequestTrainingDataCommand.java ================================================ package net.demilich.metastone.gui.trainingmode; import net.demilich.metastone.trainingmode.ITrainingDataListener; import net.demilich.metastone.trainingmode.RequestTrainingDataNotification; import net.demilich.metastone.trainingmode.TrainingData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.demilich.nittygrittymvc.SimpleCommand; import net.demilich.nittygrittymvc.interfaces.INotification; import net.demilich.metastone.GameNotification; public class RequestTrainingDataCommand extends SimpleCommand { private static Logger logger = LoggerFactory.getLogger(RequestTrainingDataCommand.class); @Override public void execute(INotification notification) { RequestTrainingDataNotification trainingDataNotification = (RequestTrainingDataNotification) notification; TrainingProxy trainingProxy = (TrainingProxy) getFacade().retrieveProxy(TrainingProxy.NAME); TrainingData trainingData = trainingProxy.getTrainingData(trainingDataNotification.getDeckName()); if (trainingData == null) { logger.debug("No training data found for " + trainingDataNotification.getDeckName()); } else { logger.debug("Training data found for " + trainingDataNotification.getDeckName()); } ITrainingDataListener listener = trainingDataNotification.getListener(); listener.answerTrainingData(trainingData); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/trainingmode/SaveTrainingDataCommand.java ================================================ package net.demilich.metastone.gui.trainingmode; import net.demilich.metastone.trainingmode.TrainingData; import net.demilich.nittygrittymvc.SimpleCommand; import net.demilich.nittygrittymvc.interfaces.INotification; import net.demilich.metastone.GameNotification; public class SaveTrainingDataCommand extends SimpleCommand { @Override public void execute(INotification notification) { TrainingData trainingData = (TrainingData) notification.getBody(); TrainingProxy trainingProxy = (TrainingProxy) getFacade().retrieveProxy(TrainingProxy.NAME); trainingProxy.saveTrainingData(trainingData); ; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/trainingmode/TrainingConfig.java ================================================ package net.demilich.metastone.gui.trainingmode; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import net.demilich.metastone.game.decks.Deck; public class TrainingConfig { private int numberOfGames; private final Deck deckToTrain; private final List decks = new ArrayList(); public TrainingConfig(Deck deckToTrain) { this.deckToTrain = deckToTrain; } public List getDecks() { return decks; } public Deck getDeckToTrain() { return deckToTrain; } public int getNumberOfGames() { return numberOfGames; } public Deck getRandomDeck() { return decks.get(ThreadLocalRandom.current().nextInt(decks.size())); } public void setNumberOfGames(int numberOfGames) { this.numberOfGames = numberOfGames; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/trainingmode/TrainingConfigView.java ================================================ package net.demilich.metastone.gui.trainingmode; 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.decks.Deck; import net.demilich.metastone.game.decks.DeckFormat; import net.demilich.metastone.game.entities.heroes.HeroClass; import net.demilich.metastone.gui.common.DeckStringConverter; public class TrainingConfigView extends BorderPane { @FXML private ComboBox numberOfGamesBox; @FXML private ComboBox deckBox; @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 TrainingConfigView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/TrainingConfigView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } setupDeckBox(); 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(); Deck deckToTrain = deckBox.getSelectionModel().getSelectedItem(); Collection decks = selectedDecksListView.getItems(); TrainingConfig trainingConfig = new TrainingConfig(deckToTrain); trainingConfig.setNumberOfGames(numberOfGames); trainingConfig.getDecks().addAll(decks); NotificationProxy.sendNotification(GameNotification.COMMIT_TRAININGMODE_CONFIG, trainingConfig); } public void injectDecks(List decks) { List filteredDecks = FXCollections.observableArrayList(); for (Deck deck : decks) { if (deck.getHeroClass() != HeroClass.DECK_COLLECTION) { filteredDecks.add(deck); } } selectedDecksListView.getItems().clear(); availableDecksListView.getItems().setAll(filteredDecks); deckBox.getItems().setAll(filteredDecks); deckBox.getSelectionModel().selectFirst(); } private void setupDeckBox() { deckBox.setConverter(new DeckStringConverter()); } private void setupNumberOfGamesBox() { ObservableList numberOfGamesEntries = FXCollections.observableArrayList(); numberOfGamesEntries.add(1); numberOfGamesEntries.add(10); numberOfGamesEntries.add(100); numberOfGamesEntries.add(1000); numberOfGamesEntries.add(10000); numberOfGamesBox.setItems(numberOfGamesEntries); numberOfGamesBox.getSelectionModel().select(2); } public void injectDeckFormats(List body) { // this.deckFormats = deckFormats; // player1Config.setDeckFormat(deckFormats.get(0)); // player2Config.setDeckFormat(deckFormats.get(0)); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/trainingmode/TrainingModeMediator.java ================================================ package net.demilich.metastone.gui.trainingmode; 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 TrainingModeMediator extends Mediator { public static final String NAME = "TrainingModeMediator"; private final TrainingConfigView configView; private final TrainingModeView view; public TrainingModeMediator() { super(NAME); configView = new TrainingConfigView(); view = new TrainingModeView(); } @SuppressWarnings("unchecked") @Override public void handleNotification(final INotification notification) { switch (notification.getId()) { case TRAINING_PROGRESS_UPDATE: TrainingProgressReport progress = (TrainingProgressReport) notification.getBody(); Platform.runLater(new Runnable() { @Override public void run() { view.showProgress(progress); } }); break; case COMMIT_TRAININGMODE_CONFIG: getFacade().sendNotification(GameNotification.SHOW_VIEW, view); TrainingConfig trainingConfig = (TrainingConfig) notification.getBody(); view.setDeckName(trainingConfig.getDeckToTrain().getName()); view.startTraining(); getFacade().sendNotification(GameNotification.START_TRAINING, trainingConfig); break; case REPLY_DECKS: configView.injectDecks((List) notification.getBody()); break; case REPLY_DECK_FORMATS: configView.injectDeckFormats((List) notification.getBody()); break; default: break; } } @Override public List listNotificationInterests() { List notificationInterests = new ArrayList(); notificationInterests.add(GameNotification.TRAINING_PROGRESS_UPDATE); notificationInterests.add(GameNotification.COMMIT_TRAININGMODE_CONFIG); notificationInterests.add(GameNotification.REPLY_DECKS); notificationInterests.add(GameNotification.REPLY_DECK_FORMATS); return notificationInterests; } @Override public void onRegister() { getFacade().sendNotification(GameNotification.SHOW_VIEW, configView); getFacade().sendNotification(GameNotification.REQUEST_DECKS); getFacade().sendNotification(GameNotification.REQUEST_DECK_FORMATS); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/trainingmode/TrainingModeView.java ================================================ package net.demilich.metastone.gui.trainingmode; import java.io.IOException; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.chart.LineChart; import javafx.scene.chart.XYChart; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.BorderPane; import net.demilich.metastone.GameNotification; import net.demilich.metastone.NotificationProxy; public class TrainingModeView extends BorderPane implements EventHandler { @FXML private Button backButton; @FXML private Label trainingLabel; @FXML private Label progressLabel; @FXML private LineChart resultChart; private XYChart.Series series; public TrainingModeView() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/TrainingModeView.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } backButton.setOnAction(this); resultChart.setVisible(false); } @Override public void handle(ActionEvent actionEvent) { if (actionEvent.getSource() == backButton) { NotificationProxy.sendNotification(GameNotification.MAIN_MENU); } } public void setDeckName(String deckname) { trainingLabel.setText("Training: " + deckname); } public void showProgress(TrainingProgressReport progress) { progressLabel.setText(progress.getGamesCompleted() + " out of " + progress.getGamesTotal() + " games completed"); int progressMark = Math.max(progress.getGamesTotal() / 100, 10); if (progress.getGamesCompleted() == 0 || progress.getGamesCompleted() % progressMark != 0) { return; } double winRate = progress.getGamesWon() / (double) progress.getGamesCompleted(); series.getData().add(new XYChart.Data(progress.getGamesCompleted(), winRate)); if (progress.getGamesCompleted() == progress.getGamesTotal()) { backButton.setDisable(false); } } public void startTraining() { resultChart.getData().clear(); backButton.setDisable(true); resultChart.setVisible(true); series = new XYChart.Series<>(); series.setName("Win rate"); resultChart.getData().add(series); series.getData().add(new XYChart.Data(0, 0)); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/trainingmode/TrainingProgressReport.java ================================================ package net.demilich.metastone.gui.trainingmode; public class TrainingProgressReport { private final int gamesCompleted; private final int gamesTotal; private final int gamesWon; public TrainingProgressReport(int gamesCompleted, int gamesTotal, int gamesWon) { this.gamesCompleted = gamesCompleted; this.gamesTotal = gamesTotal; this.gamesWon = gamesWon; } public int getGamesCompleted() { return gamesCompleted; } public int getGamesTotal() { return gamesTotal; } public int getGamesWon() { return gamesWon; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/gui/trainingmode/TrainingProxy.java ================================================ package net.demilich.metastone.gui.trainingmode; 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.Paths; import java.util.Collection; import java.util.HashMap; 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.behaviour.threat.FeatureVector; import net.demilich.metastone.game.behaviour.threat.WeightedFeature; import net.demilich.metastone.trainingmode.TrainingData; import net.demilich.metastone.utils.ResourceInputStream; import net.demilich.metastone.utils.ResourceLoader; import net.demilich.metastone.utils.UserHomeMetastone; import net.demilich.nittygrittymvc.Proxy; public class TrainingProxy extends Proxy { public static final String NAME = "TrainingProxy"; private static final String TRAINING_FOLDER = "training"; private static final String TRAINING_FOLDER_PATH = UserHomeMetastone.getPath() + File.separator + TRAINING_FOLDER; private static Logger logger = LoggerFactory.getLogger(TrainingProxy.class); private final HashMap trainingData = new HashMap(); public TrainingProxy() { super(NAME); if (new File(TRAINING_FOLDER_PATH).mkdir()) { logger.info(TRAINING_FOLDER_PATH + " folder created"); } try { loadTrainingData(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (URISyntaxException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } public TrainingData getTrainingData(String deckName) { return trainingData.containsKey(deckName) ? new TrainingData(deckName, trainingData.get(deckName)) : null; } public void loadTrainingData() throws IOException, URISyntaxException { trainingData.clear(); // load training from resources jar on the classpath Collection inputStreams = ResourceLoader.loadJsonInputStreams(TRAINING_FOLDER, false); // load cards from ~/metastone/training folder on the filesystem if (Paths.get(TRAINING_FOLDER_PATH).toFile().exists()) { inputStreams.addAll((ResourceLoader.loadJsonInputStreams(TRAINING_FOLDER_PATH, true))); } Gson gson = new GsonBuilder().setPrettyPrinting().create(); HashMap map; Reader reader; for (ResourceInputStream resourceInputStream : inputStreams) { reader = new InputStreamReader(resourceInputStream.inputStream); map = gson.fromJson(reader, new TypeToken>() {}.getType()); final String DECK_NAME = "deck"; if (!map.containsKey(DECK_NAME)) { logger.error("Training data {} does not specify a value for '{}' and is therefor not valid", resourceInputStream.fileName, DECK_NAME); continue; } String deckName = (String) map.get(DECK_NAME); map.remove(DECK_NAME); FeatureVector featureVector = new FeatureVector(); for (String element : map.keySet()) { try { WeightedFeature feature = WeightedFeature.valueOf(element); double value = (double) map.get(element); featureVector.set(feature, value); } catch (IllegalArgumentException ex) { logger.warn("Illegal argument in training data: " + element + " Value: " + map.get(element)); } } trainingData.put(deckName, featureVector); } } public void saveTrainingData(TrainingData trainingData) { String deckName = trainingData.getDeckName(); FeatureVector featureVector = trainingData.getFeatureVector(); this.trainingData.put(deckName, featureVector); Gson gson = new GsonBuilder().setPrettyPrinting().create(); HashMap saveData = new HashMap(); saveData.put("deck", deckName); for (WeightedFeature feature : featureVector.getValues().keySet()) { double value = featureVector.get(feature); saveData.put(feature.toString(), value); } String jsonData = gson.toJson(saveData); try { String filename = deckName.toLowerCase(); filename = filename.replaceAll(" ", "_"); filename = filename.replaceAll("\\W+", ""); filename = TRAINING_FOLDER_PATH + File.separator + filename + ".json"; Files.write(Paths.get(filename), jsonData.getBytes()); } catch (IOException e) { e.printStackTrace(); } } } ================================================ FILE: app/src/main/java/net/demilich/metastone/tools/CardCreator.java ================================================ package net.demilich.metastone.tools; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.StageStyle; public class CardCreator extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) throws Exception { primaryStage.setTitle("Card Creator"); primaryStage.initStyle(StageStyle.UNIFIED); Scene scene = new Scene(new EditorMainWindow()); // scene.getStylesheets().add(IconFactory.class.getResource("main.css").toExternalForm()); primaryStage.setScene(scene); primaryStage.show(); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/tools/CardEditor.java ================================================ package net.demilich.metastone.tools; import java.io.File; import java.io.IOException; import org.apache.commons.io.FileUtils; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.control.ComboBox; import javafx.scene.layout.VBox; import net.demilich.metastone.game.spells.Spell; import net.demilich.metastone.gui.common.ComboBoxKeyHandler; public abstract class CardEditor extends VBox implements ICardEditor { public CardEditor(String fxmlFile) { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/" + fxmlFile)); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } } @SuppressWarnings("unchecked") protected void fillWithSpells(ComboBox> comboBox) { ObservableList> items = FXCollections.observableArrayList(); String spellPath = "./src/main/java/" + Spell.class.getPackage().getName().replace(".", "/") + "/"; for (File file : FileUtils.listFiles(new File(spellPath), new String[] { "java" }, false)) { String fileName = file.getName().replace(".java", ""); String spellClassName = Spell.class.getPackage().getName() + "." + fileName; Class spellClass = null; try { spellClass = (Class) Class.forName(spellClassName); } catch (ClassNotFoundException e) { e.printStackTrace(); } items.add(spellClass); } comboBox.setItems(items); comboBox.setOnKeyReleased(new ComboBoxKeyHandler>(comboBox)); } @Override public Node getPanel() { return this; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/tools/EditorMainWindow.java ================================================ package net.demilich.metastone.tools; import java.awt.Desktop; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.List; import org.apache.commons.lang3.StringUtils; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import javafx.beans.value.ObservableValue; 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.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.TextField; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; import javafx.stage.FileChooser; import net.demilich.metastone.game.Attribute; import net.demilich.metastone.game.cards.CardSet; import net.demilich.metastone.game.cards.Rarity; import net.demilich.metastone.game.cards.desc.CardDesc; import net.demilich.metastone.game.cards.desc.ParseUtils; import net.demilich.metastone.game.entities.heroes.HeroClass; import net.demilich.metastone.game.spells.desc.SpellDesc; import net.demilich.metastone.gui.common.ComboBoxKeyHandler; class EditorMainWindow extends BorderPane { private static String getCardId(CardDesc card) { String result = ""; String prefix = ""; switch (card.type) { case HERO_POWER: prefix = "hero_power_"; break; case MINION: prefix = card.collectible ? "minion_" : "token_"; break; case SPELL: case CHOOSE_ONE: prefix = "spell_"; break; case WEAPON: prefix = "weapon_"; break; default: break; } for (String word : card.name.split(" ")) { String cleansedWord = word.replace("'", "").replace(":", ""); result += prefix + cleansedWord.toLowerCase(); prefix = "_"; } return result; } @FXML private RadioButton minionRadioButton; @FXML private RadioButton spellRadioButton; @FXML private RadioButton weaponRadioButton; @FXML private TextField nameField; @FXML private Label idLabel; @FXML private TextField descriptionField; @FXML private ComboBox rarityBox; @FXML private ComboBox heroClassBox; @FXML private ComboBox cardSetBox; @FXML private TextField manaCostField; @FXML private CheckBox collectibleBox; @FXML private Pane contentPanel; @FXML private Button resetButton; @FXML private Button saveButton; private final ToggleGroup cardTypeGroup = new ToggleGroup(); private List> attributeBoxes; private List attributeFields; private ICardEditor cardEditor; private CardDesc card; public EditorMainWindow() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/EditorMainWindow.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } minionRadioButton.setToggleGroup(cardTypeGroup); spellRadioButton.setToggleGroup(cardTypeGroup); weaponRadioButton.setToggleGroup(cardTypeGroup); minionRadioButton.setSelected(true); minionRadioButton.setOnAction(event -> setCardEditor(new MinionCardPanel())); spellRadioButton.setOnAction(event -> setCardEditor(new SpellCardPanel())); nameField.textProperty().addListener(this::onNameChanged); descriptionField.textProperty().addListener(this::onDescriptionChanged); rarityBox.setItems(FXCollections.observableArrayList(Rarity.values())); heroClassBox.setItems(FXCollections.observableArrayList(HeroClass.values())); cardSetBox.setItems(FXCollections.observableArrayList(CardSet.values())); setCardEditor(new MinionCardPanel()); rarityBox.valueProperty().addListener(this::onRarityChanged); resetButton.setOnAction(this::reset); saveButton.setOnAction(this::onSaveButton); heroClassBox.valueProperty().addListener(this::onHeroClassChanged); cardSetBox.valueProperty().addListener(this::onCardSetChanged); collectibleBox.setOnAction(this::onCollectibleChanged); manaCostField.textProperty().addListener(new IntegerListener(value -> card.baseManaCost = value)); attributeBoxes = new ArrayList<>(); attributeFields = new ArrayList<>(); for (int i = 1; i < 99; i++) { @SuppressWarnings("unchecked") ComboBox box = (ComboBox) lookup("#attributeBox" + i); if (box == null) { break; } TextField field = (TextField) lookup("#attributeField" + i); attributeBoxes.add(box); attributeFields.add(field); } setupAttributeBoxes(); } private Object getAttributeValue(String valueString) { Object value = null; if (ParseUtils.tryParseInt(valueString)) { value = Integer.parseInt(valueString); } else if (ParseUtils.tryParseBool(valueString)) { value = Boolean.parseBoolean(valueString); } else { value = valueString; } return value; } private void onAttributesChanged() { card.attributes = new EnumMap(Attribute.class); for (int i = 0; i < attributeBoxes.size(); i++) { ComboBox attributeBox = attributeBoxes.get(i); TextField attributeField = attributeFields.get(i); if (attributeBox.getSelectionModel().getSelectedItem() == null) { continue; } if (StringUtils.isEmpty(attributeField.getText())) { attributeField.setText("true"); } Attribute attribute = attributeBox.getSelectionModel().getSelectedItem(); Object value = getAttributeValue(attributeField.getText()); card.attributes.put(attribute, value); } } private void onCardSetChanged(ObservableValue ov, CardSet oldCardSet, CardSet newCardSet) { card.set = newCardSet; } private void onCollectibleChanged(ActionEvent event) { card.collectible = collectibleBox.isSelected(); } private void onDescriptionChanged(ObservableValue ov, String oldValue, String newValue) { card.description = newValue; } private void onHeroClassChanged(ObservableValue ov, HeroClass oldHeroClass, HeroClass newHeroClass) { card.heroClass = newHeroClass; } private void onNameChanged(ObservableValue ov, String oldValue, String newValue) { card.name = newValue; card.id = getCardId(card); idLabel.setText(card.id); } private void onRarityChanged(ObservableValue ov, Rarity oldRarity, Rarity newRarity) { card.rarity = newRarity; } private void onSaveButton(ActionEvent event) { save(); } private void reset(ActionEvent event) { for (int i = 0; i < attributeBoxes.size(); i++) { attributeBoxes.get(i).valueProperty().set(null); } card.attributes = null; cardEditor.reset(); } private void save() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Save card"); fileChooser.setInitialDirectory(new File("./cards/")); fileChooser.setInitialFileName(card.id + ".json"); File file = fileChooser.showSaveDialog(getScene().getWindow()); if (file == null) { return; } System.out.println("Saving to: " + file.getName()); GsonBuilder builder = new GsonBuilder().setPrettyPrinting(); builder.disableHtmlEscaping(); builder.registerTypeAdapter(SpellDesc.class, new SpellDescSerializer()); Gson gson = builder.create(); String json = gson.toJson(card); try { // FileUtils.writeStringToFile(file, json); Path path = Paths.get(file.getPath()); Files.write(path, json.getBytes()); Desktop.getDesktop().open(file); } catch (IOException e) { e.printStackTrace(); } } private void setCardEditor(ICardEditor cardEditor) { this.cardEditor = cardEditor; CardDesc newCard = cardEditor.getCardDesc(); if (card != null) { newCard.name = card.name; newCard.description = card.description; newCard.rarity = card.rarity; newCard.heroClass = card.heroClass; newCard.baseManaCost = card.baseManaCost; } else { newCard.name = ""; newCard.rarity = Rarity.FREE; newCard.heroClass = HeroClass.ANY; newCard.set = CardSet.CUSTOM; newCard.baseManaCost = 0; newCard.collectible = true; } card = newCard; card.id = getCardId(card); contentPanel.getChildren().setAll(cardEditor.getPanel()); // update ui nameField.setText(card.name); idLabel.setText(card.id); descriptionField.setText(card.description); rarityBox.getSelectionModel().select(card.rarity); heroClassBox.getSelectionModel().select(card.heroClass); cardSetBox.getSelectionModel().select(card.set); manaCostField.setText(String.valueOf(card.baseManaCost)); collectibleBox.setSelected(card.collectible); } private void setupAttributeBoxes() { for (ComboBox comboBox : attributeBoxes) { ObservableList items = FXCollections.observableArrayList(); items.addAll(Attribute.values()); Collections.sort(items, (obj1, obj2) -> { if (obj1 == obj2) { return 0; } if (obj1 == null) { return -1; } if (obj2 == null) { return 1; } return obj1.toString().compareTo(obj2.toString()); }); comboBox.setItems(items); comboBox.valueProperty().addListener((ov, oldValue, newValue) -> onAttributesChanged()); comboBox.setOnKeyReleased(new ComboBoxKeyHandler(comboBox)); } for (TextField attributeField : attributeFields) { attributeField.textProperty().addListener((ov, oldValue, newValue) -> onAttributesChanged()); } } } ================================================ FILE: app/src/main/java/net/demilich/metastone/tools/ICardEditor.java ================================================ package net.demilich.metastone.tools; import javafx.scene.Node; import net.demilich.metastone.game.cards.desc.CardDesc; public interface ICardEditor { public CardDesc getCardDesc(); public Node getPanel(); public void reset(); } ================================================ FILE: app/src/main/java/net/demilich/metastone/tools/ITextFieldAction.java ================================================ package net.demilich.metastone.tools; interface ITextFieldAction { public void onChanged(int value); } ================================================ FILE: app/src/main/java/net/demilich/metastone/tools/IntegerListener.java ================================================ package net.demilich.metastone.tools; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.scene.control.TextField; public class IntegerListener implements ChangeListener { private final ITextFieldAction action; public IntegerListener(ITextFieldAction action) { this.action = action; } @Override public void changed(ObservableValue observable, String oldText, String newText) { if (newText.matches("\\d*")) { int value = Integer.parseInt(newText); action.onChanged(value); } else { TextField textField = (TextField) observable; textField.setText(oldText); } } } ================================================ FILE: app/src/main/java/net/demilich/metastone/tools/MinionCardPanel.java ================================================ package net.demilich.metastone.tools; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; import net.demilich.metastone.game.cards.CardType; import net.demilich.metastone.game.cards.desc.CardDesc; import net.demilich.metastone.game.cards.desc.MinionCardDesc; import net.demilich.metastone.game.entities.minions.Race; import net.demilich.metastone.game.spells.Spell; import net.demilich.metastone.game.spells.desc.BattlecryDesc; import net.demilich.metastone.game.spells.desc.SpellDesc; import net.demilich.metastone.game.targeting.TargetSelection; class MinionCardPanel extends CardEditor { @FXML private ComboBox raceBox; @FXML private TextField attackField; @FXML private TextField hpField; @FXML private ComboBox> battlecrySpellBox; @FXML private ComboBox battlecryTargetSelectionBox; @FXML private ComboBox> deathrattleSpellBox; private final MinionCardDesc card = new MinionCardDesc(); public MinionCardPanel() { super("MinionCardPanel.fxml"); raceBox.setItems(FXCollections.observableArrayList(Race.values())); raceBox.valueProperty().addListener(this::onRaceChanged); battlecryTargetSelectionBox.setItems(FXCollections.observableArrayList(TargetSelection.values())); battlecryTargetSelectionBox.getSelectionModel().selectFirst(); battlecryTargetSelectionBox.valueProperty().addListener(this::onTargetSelectionChanged); battlecrySpellBox.setConverter(new SpellStringConverter()); fillWithSpells(battlecrySpellBox); battlecrySpellBox.valueProperty().addListener(this::onBattlecryChanged); deathrattleSpellBox.setConverter(new SpellStringConverter()); fillWithSpells(deathrattleSpellBox); deathrattleSpellBox.valueProperty().addListener(this::onDeathrattleChanged); attackField.textProperty().addListener(new IntegerListener(value -> card.baseAttack = value)); hpField.textProperty().addListener(new IntegerListener(value -> card.baseHp = value)); } @Override public CardDesc getCardDesc() { card.type = CardType.MINION; card.name = ""; return card; } private void onBattlecryChanged(ObservableValue> ov, Class oldSpell, Class newSpell) { SpellDesc spell = new SpellDesc(SpellDesc.build(newSpell)); if (card.battlecry == null) { card.battlecry = new BattlecryDesc(); } card.battlecry.spell = spell; } private void onDeathrattleChanged(ObservableValue> ov, Class oldSpell, Class newSpell) { card.deathrattle = new SpellDesc(SpellDesc.build(newSpell)); } private void onRaceChanged(ObservableValue ov, Race oldRace, Race newRace) { card.race = newRace != Race.NONE ? newRace : null; } private void onTargetSelectionChanged(ObservableValue ov, TargetSelection oldValue, TargetSelection newValue) { if (card.battlecry == null) { card.battlecry = new BattlecryDesc(); } card.battlecry.targetSelection = newValue; } @Override public void reset() { battlecrySpellBox.valueProperty().set(null); battlecryTargetSelectionBox.getSelectionModel().select(TargetSelection.NONE); card.battlecry = null; deathrattleSpellBox.valueProperty().set(null); card.deathrattle = null; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/tools/SpellCardPanel.java ================================================ package net.demilich.metastone.tools; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.scene.control.ComboBox; import net.demilich.metastone.game.cards.CardType; import net.demilich.metastone.game.cards.desc.CardDesc; import net.demilich.metastone.game.cards.desc.SpellCardDesc; import net.demilich.metastone.game.spells.Spell; import net.demilich.metastone.game.spells.desc.SpellDesc; import net.demilich.metastone.game.targeting.TargetSelection; public class SpellCardPanel extends CardEditor { @FXML private ComboBox> spellBox; @FXML private ComboBox targetSelectionBox; private SpellCardDesc card = new SpellCardDesc(); public SpellCardPanel() { super("SpellCardPanel.fxml"); targetSelectionBox.setItems(FXCollections.observableArrayList(TargetSelection.values())); targetSelectionBox.getSelectionModel().selectFirst(); targetSelectionBox.valueProperty().addListener(this::onTargetSelectionChanged); spellBox.setConverter(new SpellStringConverter()); fillWithSpells(spellBox); spellBox.valueProperty().addListener(this::onSpellChanged); } @Override public CardDesc getCardDesc() { card.type = CardType.SPELL; card.targetSelection = TargetSelection.NONE; card.name = ""; return card; } private void onSpellChanged(ObservableValue> ov, Class oldSpell, Class newSpell) { card.spell = new SpellDesc(SpellDesc.build(newSpell)); } private void onTargetSelectionChanged(ObservableValue ov, TargetSelection oldValue, TargetSelection newValue) { card.targetSelection = newValue; } @Override public void reset() { spellBox.valueProperty().set(null); card.spell = null; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/tools/SpellDescSerializer.java ================================================ package net.demilich.metastone.tools; import java.lang.reflect.Type; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import net.demilich.metastone.game.cards.desc.ParseUtils; import net.demilich.metastone.game.spells.desc.SpellArg; import net.demilich.metastone.game.spells.desc.SpellDesc; public class SpellDescSerializer implements JsonSerializer { @Override public JsonElement serialize(SpellDesc spell, Type type, JsonSerializationContext context) { JsonObject result = new JsonObject(); result.add("class", new JsonPrimitive(spell.getSpellClass().getSimpleName())); for (SpellArg spellArg : SpellArg.values()) { if (spellArg == SpellArg.CLASS) { continue; } if (!spell.contains(spellArg)) { continue; } String argName = ParseUtils.toCamelCase(spellArg.toString()); result.add(argName, new JsonPrimitive(spell.get(spellArg).toString())); } return result; } } ================================================ FILE: app/src/main/java/net/demilich/metastone/tools/SpellStringConverter.java ================================================ package net.demilich.metastone.tools; import javafx.util.StringConverter; import net.demilich.metastone.game.spells.Spell; public class SpellStringConverter extends StringConverter> { @Override public Class fromString(String arg0) { return null; } @Override public String toString(Class spell) { return spell.getSimpleName(); } } ================================================ FILE: app/src/main/java/net/demilich/metastone/tools/WeaponClassPanel.java ================================================ package net.demilich.metastone.tools; import net.demilich.metastone.game.cards.CardType; import net.demilich.metastone.game.cards.desc.CardDesc; import net.demilich.metastone.game.cards.desc.WeaponCardDesc; public class WeaponClassPanel extends CardEditor { private final WeaponCardDesc card = new WeaponCardDesc(); public WeaponClassPanel() { super("WeaponCardPanel.fxml"); } @Override public CardDesc getCardDesc() { card.type = CardType.WEAPON; card.name = ""; return card; } @Override public void reset() { card.battlecry = null; card.deathrattle = null; } } ================================================ FILE: app/src/main/resources/css/deckbuilder.css ================================================ .class-button { -fx-font-size: 20pt; -fx-font-family: "System"; } .card-entry-name { -fx-font-size: 10pt; -fx-font-family: "System"; -fx-fill: black; -fx-font-weight: bold; -fx-wrap-text: true; } .card-entry { -fx-border-width: 2; -fx-border-color: black; -fx-background-color: tan; -fx-border-radius: 10; } .card-entry:hover { -fx-border-width: 3; -fx-border-color: yellow; -fx-background-color: silver; -fx-border-radius: 10; } .delete-button { -fx-background-color: transparent; -fx-background-radius: 0; -fx-background-insets: 0; -fx-padding: 4; } .delete-button:hover { -fx-background-color: transparent; -fx-scale-x: 1.2; -fx-scale-y: 1.2; -fx-scale-z: 1.2; } ================================================ FILE: app/src/main/resources/css/gameboard.css ================================================ .board { -fx-background-color: bisque; } .center-message { -fx-font-size: 20pt; -fx-font-family: "System"; -fx-font-weight: bold; -fx-text-fill: gray; -fx-font-weight: bold; -fx-font-style: italic; } #target-button { -fx-background-color: transparent; } ================================================ FILE: app/src/main/resources/css/main.css ================================================ .button { -fx-background-color: #090a0c, linear-gradient(#38424b 0%, #1f2429 20%, #191d22 100%), linear-gradient(#20262b, #191d22), radial-gradient(center 50% 0%, radius 100%, rgba(114,131,148,0.9), rgba(255,255,255,0)); -fx-background-radius: 5,4,3,5; -fx-background-insets: 0,1,2,0; -fx-text-fill: white; -fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.6) , 5, 0.0 , 0 , 1 ); -fx-font-family: "System"; -fx-text-fill: linear-gradient(white, #d0d0d0); -fx-font-size: 16px; -fx-padding: 8 16 8 16; } .button:hover { -fx-background-color: #090a0c, linear-gradient(#38424b 0%, #1f2429 20%, #191d22 100%), linear-gradient(#10161b, #090d12), radial-gradient(center 50% 0%, radius 100%, rgba(84,101,118,0.9), rgba(128,128,128,0)); -fx-text-fill: darkorange; } .combo-box { -fx-background-color: #090a0c, linear-gradient(#38424b 0%, #1f2429 20%, #191d22 100%), linear-gradient(#20262b, #191d22), radial-gradient(center 50% 0%, radius 100%, rgba(114,131,148,0.9), rgba(255,255,255,0)); -fx-background-radius: 5,4,3,5; -fx-background-insets: 0,1,2,0; -fx-font-family: "System"; -fx-text-fill: black; -fx-font-size: 12px; } .combo-box .list-cell:selected { -fx-text-fill: white; } .panel { -fx-background-color: dimgray; } .label { -fx-font-family: "System"; -fx-text-fill: black; } .progress-bar .bar { -fx-background-color: linear-gradient(to bottom, derive(darkorange, -22%), derive(darkorange, 3%), derive(darkorange, -18%), derive(darkorange, -24%) ); -fx-background-insets: 3 3 4 3; -fx-background-radius: 2; -fx-padding: 0.75em; } .progress-bar .track { -fx-background-color: -fx-shadow-highlight-color, linear-gradient(to bottom, derive(dimgray, 20%), dimgray), linear-gradient(to bottom, derive(darkgray, 3%), derive(darkgray, 10%), derive(darkgray, 7%), derive(darkgray, 1%) ); } .scroll-pane { -fx-background-color: linen; -fx-control-inner-background: linen; } .bordered-panel { -fx-background-color: darkgray; -fx-background-radius: 10; -fx-border-width: 2; -fx-border-color: darkslategrey; -fx-border-radius: 10; -fx-border-insets: 4; } .bordered-dialog { -fx-background-color: linen; -fx-background-radius: 10; -fx-border-width: 3; -fx-border-color: darkslategrey; -fx-border-radius: 10; -fx-border-insets: 4; } .dialog-text { -fx-font-size: 10pt; -fx-font-family: "System"; -fx-text-fill: black; } .sidebar { -fx-background: tan; -fx-border-width: 2; -fx-border-color: darkslategrey; -fx-background-color: tan; -fx-border-radius: 10; } .header { -fx-font-size: 24pt; -fx-font-family: "System"; -fx-font-weight: bold; -fx-fill: black; } .card-border { -fx-background-color: navajowhite; -fx-border-color: black; -fx-border-style: solid; -fx-border-width: 1.0; } .outlined-score { -fx-font-size: 14pt; -fx-font-family: "System"; -fx-font-weight: bold; -fx-fill: white; -fx-stroke: black; -fx-stroke-width: 2; } .outlined-score-yellow { -fx-font-size: 14pt; -fx-font-family: "System"; -fx-font-weight: bold; -fx-fill: yellow; -fx-stroke: black; -fx-stroke-width: 2; } .tooltip-description { -fx-font-size: 11pt; -fx-font-family: "System"; -fx-text-fill: black; } .race-label { -fx-font-size: 8pt; -fx-font-family: "System"; -fx-text-fill: black; -fx-font-weight: bold; -fx-font-style : italic; } .name-big { -fx-font-size: 14pt; -fx-font-family: "System"; -fx-font-weight: bold; -fx-text-fill: black; } .name-small { -fx-font-size: 10pt; -fx-font-family: "System"; -fx-font-weight: bold; -fx-text-fill: black; } .progress-text { -fx-font-size: 8pt; -fx-font-family: "System"; -fx-font-weight: bold; -fx-text-fill: black; } .info-small { -fx-font-size: 10pt; -fx-font-family: "System"; -fx-text-fill: black; } .label-white { -fx-font-family: "System"; -fx-text-fill: white; } .default-label { -fx-font-size: 18pt; -fx-font-family: "System"; -fx-fill: dimgray; } .separator { -fx-background-color: dimgray; -fx-background-radius: 2; } .separator *.line { -fx-border-style: solid; -fx-border-width: 0.5px; } .text-field { -fx-background-color: antiquewhite; -fx-border-width: 1; -fx-border-color: black; } .mana-label { -fx-font-size: 12pt; -fx-font-family: "System"; -fx-text-fill: blue; -fx-font-weight: bold; } .card-entry { -fx-border-width: 2; -fx-border-color: black; -fx-background-color: tan; -fx-border-radius: 10; -fx-background-radius: 10; } ================================================ FILE: app/src/main/resources/css/mainmenu.css ================================================ .title { -fx-font-size: 32pt; -fx-font-family: "System"; -fx-font-weight: bold; -fx-effect: dropshadow(one-pass-box, black, 6.0, 0.9, 1.0, 1.0); -fx-fill: white; } .main-button { -fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.6) , 5, 0.0 , 0 , 1 ); -fx-font-family: "System"; -fx-text-fill: linear-gradient(white, #d0d0d0); -fx-font-size: 20px; -fx-font-weight: bold; } .main-button:hover { -fx-text-fill: darkorange; } ================================================ FILE: app/src/main/resources/fxml/BattleBatchResultToken.fxml ================================================
================================================ FILE: app/src/main/resources/fxml/BattleOfDecksConfigView.fxml ================================================
================================================ FILE: app/src/main/resources/fxml/DeckBuilderView.fxml ================================================
================================================ FILE: app/src/main/resources/fxml/DeckInfoView.fxml ================================================ ================================================ FILE: app/src/main/resources/fxml/DeckNameView.fxml ================================================ ================================================ FILE: app/src/main/resources/fxml/DigitTemplate.fxml ================================================ ================================================ FILE: app/src/main/resources/fxml/EditorMainWindow.fxml ================================================
================================================ FILE: app/src/main/resources/fxml/LoadingBoardView.fxml ================================================
================================================ FILE: app/src/main/resources/fxml/MainMenuView.fxml ================================================
================================================ FILE: app/src/main/resources/fxml/MetaDeckListView.fxml ================================================ ================================================ FILE: app/src/main/resources/fxml/MetaDeckView (2).fxml ================================================
================================================ FILE: app/src/main/resources/fxml/MetaDeckView.fxml ================================================
================================================ FILE: app/src/main/resources/fxml/MinionCardPanel.fxml ================================================ ================================================ FILE: app/src/main/resources/fxml/MinionPanel.fxml ================================================ ================================================ FILE: app/src/main/resources/fxml/SimulationModeConfigView.fxml ================================================
================================================ FILE: app/src/main/resources/fxml/SpellCardPanel.fxml ================================================ ================================================ FILE: app/src/main/resources/fxml/SummonToken.fxml ================================================
================================================ FILE: app/src/main/resources/fxml/ToolboxView.fxml ================================================ ================================================ FILE: app/src/main/resources/fxml/TrainingConfigView.fxml ================================================