From 9f6eb989bde4327dfea79c8435d72105bab5712e Mon Sep 17 00:00:00 2001 From: Francisco Moraes Date: Tue, 5 Mar 2024 21:18:42 -0500 Subject: [PATCH] Mulligan Guide Pre Lobby --- HSTracker.xcodeproj/project.pbxproj | 38 +- .../Contents.json | 21 + .../mulligan-guide-data.png | Bin 0 -> 3754 bytes .../Contents.json | 21 + .../mulligan-guide-no-data.png | Bin 0 -> 3720 bytes HSTracker/Core/Settings.swift | 3 + HSTracker/Core/SizeHelper.swift | 95 +++-- HSTracker/Database/Models/Deck.swift | 13 + HSTracker/HSReplay/HSReplay.swift | 1 + HSTracker/HSReplay/HSReplayAPI.swift | 33 ++ HSTracker/HearthMirror-version.txt | 2 +- HSTracker/HearthMirror/MirrorHelper.swift | 16 +- HSTracker/Hearthstone/HearthDbConverter.swift | 45 ++ HSTracker/Importers/ClipboardImporter.swift | 2 +- HSTracker/Importers/DeckSerializer.swift | 117 ++++-- HSTracker/Logging/CoreManager.swift | 10 + HSTracker/Logging/Enums/Mode.swift | 2 +- HSTracker/Logging/Game.swift | 77 +++- .../Handlers/LoadingScreenHandler.swift | 17 - HSTracker/Logging/QueueEvents.swift | 3 + HSTracker/Logging/SceneHandler.swift | 79 ++++ .../ConstructedMulliganGuidePreLobby.swift | 118 ++++++ .../ConstructedMulliganGuidePreLobby.xib | 103 +++++ ...ructedMulliganGuidePreLobbyViewModel.swift | 391 ++++++++++++++++++ .../ConstructedMulliganSingleDeckStatus.swift | 64 +++ .../ConstructedMulliganSingleDeckStatus.xib | 95 +++++ HSTracker/UIs/DeckManager/DeckManager.swift | 2 +- HSTracker/UIs/DeckManager/NewDeck.swift | 2 +- .../Base.lproj/TrackersPreferences.xib | 55 ++- .../UIs/Preferences/TrackersPreferences.swift | 8 +- HSTracker/UIs/Trackers/WindowManager.swift | 4 + .../macOS/Base.lproj/Localizable.strings | 3 + 32 files changed, 1298 insertions(+), 142 deletions(-) create mode 100644 HSTracker/Assets/Assets.xcassets/mulligan-guide-data.imageset/Contents.json create mode 100644 HSTracker/Assets/Assets.xcassets/mulligan-guide-data.imageset/mulligan-guide-data.png create mode 100644 HSTracker/Assets/Assets.xcassets/mulligan-guide-no-data.imageset/Contents.json create mode 100644 HSTracker/Assets/Assets.xcassets/mulligan-guide-no-data.imageset/mulligan-guide-no-data.png create mode 100644 HSTracker/Hearthstone/HearthDbConverter.swift create mode 100644 HSTracker/Logging/SceneHandler.swift create mode 100644 HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobby.swift create mode 100644 HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobby.xib create mode 100644 HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobbyViewModel.swift create mode 100644 HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganSingleDeckStatus.swift create mode 100644 HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganSingleDeckStatus.xib diff --git a/HSTracker.xcodeproj/project.pbxproj b/HSTracker.xcodeproj/project.pbxproj index 95a61809..5dad0b6a 100644 --- a/HSTracker.xcodeproj/project.pbxproj +++ b/HSTracker.xcodeproj/project.pbxproj @@ -601,6 +601,11 @@ B8AF0C732B8439A800B65F63 /* ConstructedMulliganGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF0C722B8439A800B65F63 /* ConstructedMulliganGuide.swift */; }; B8AF0C752B843C6C00B65F63 /* ConstructedMulliganSingleCardStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF0C742B843C6C00B65F63 /* ConstructedMulliganSingleCardStats.swift */; }; B8AF0C772B85625200B65F63 /* Region.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF0C762B85625200B65F63 /* Region.swift */; }; + B8AF0C792B91444200B65F63 /* ConstructedMulliganGuidePreLobbyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF0C782B91444200B65F63 /* ConstructedMulliganGuidePreLobbyViewModel.swift */; }; + B8AF0C7B2B928AA400B65F63 /* ConstructedMulliganGuidePreLobby.xib in Resources */ = {isa = PBXBuildFile; fileRef = B8AF0C7A2B928AA400B65F63 /* ConstructedMulliganGuidePreLobby.xib */; }; + B8AF0C7D2B9295A000B65F63 /* ConstructedMulliganSingleDeckStatus.xib in Resources */ = {isa = PBXBuildFile; fileRef = B8AF0C7C2B9295A000B65F63 /* ConstructedMulliganSingleDeckStatus.xib */; }; + B8AF0C7F2B92B76700B65F63 /* ConstructedMulliganSingleDeckStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF0C7E2B92B76700B65F63 /* ConstructedMulliganSingleDeckStatus.swift */; }; + B8AF0C812B92C5E800B65F63 /* ConstructedMulliganGuidePreLobby.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF0C802B92C5E800B65F63 /* ConstructedMulliganGuidePreLobby.swift */; }; B8AF932C282D41D500864934 /* BattlegroundsTribe.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF932B282D41D500864934 /* BattlegroundsTribe.swift */; }; B8AF9330282D4A7C00864934 /* BattlegroundsSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF932F282D4A7C00864934 /* BattlegroundsSession.swift */; }; B8AF9332282D797700864934 /* BattlegroundsTribe.xib in Resources */ = {isa = PBXBuildFile; fileRef = B8AF9331282D797700864934 /* BattlegroundsTribe.xib */; }; @@ -739,6 +744,8 @@ B8DEDCEB2547CFF200DF524B /* HearthMirror.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4C68AF2A2447D35A00E52834 /* HearthMirror.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B8E76171293A321A0048E802 /* FindGameState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E76170293A321A0048E802 /* FindGameState.swift */; }; B8E76173293A3D010048E802 /* QueueEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E76172293A3D010048E802 /* QueueEvents.swift */; }; + B8E868832B93AF04005B1315 /* SceneHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E868822B93AF04005B1315 /* SceneHandler.swift */; }; + B8E868852B940EB7005B1315 /* HearthDbConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E868842B940EB7005B1315 /* HearthDbConverter.swift */; }; B8EC9A482612148500EB17A9 /* MultiIdCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8832DDB26117FCE00646BA4 /* MultiIdCard.swift */; }; B8EC9A492612149D00EB17A9 /* RepeatingTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8832DD926057A5000646BA4 /* RepeatingTimer.swift */; }; B8EC9A4A261215C800EB17A9 /* OpponentDeadForTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DC376025D06C2F00452A83 /* OpponentDeadForTracker.swift */; }; @@ -1254,6 +1261,11 @@ B8AF0C722B8439A800B65F63 /* ConstructedMulliganGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructedMulliganGuide.swift; sourceTree = ""; }; B8AF0C742B843C6C00B65F63 /* ConstructedMulliganSingleCardStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructedMulliganSingleCardStats.swift; sourceTree = ""; }; B8AF0C762B85625200B65F63 /* Region.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Region.swift; sourceTree = ""; }; + B8AF0C782B91444200B65F63 /* ConstructedMulliganGuidePreLobbyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructedMulliganGuidePreLobbyViewModel.swift; sourceTree = ""; }; + B8AF0C7A2B928AA400B65F63 /* ConstructedMulliganGuidePreLobby.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConstructedMulliganGuidePreLobby.xib; sourceTree = ""; }; + B8AF0C7C2B9295A000B65F63 /* ConstructedMulliganSingleDeckStatus.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConstructedMulliganSingleDeckStatus.xib; sourceTree = ""; }; + B8AF0C7E2B92B76700B65F63 /* ConstructedMulliganSingleDeckStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructedMulliganSingleDeckStatus.swift; sourceTree = ""; }; + B8AF0C802B92C5E800B65F63 /* ConstructedMulliganGuidePreLobby.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructedMulliganGuidePreLobby.swift; sourceTree = ""; }; B8AF932B282D41D500864934 /* BattlegroundsTribe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BattlegroundsTribe.swift; sourceTree = ""; }; B8AF932F282D4A7C00864934 /* BattlegroundsSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BattlegroundsSession.swift; sourceTree = ""; }; B8AF9331282D797700864934 /* BattlegroundsTribe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BattlegroundsTribe.xib; sourceTree = ""; }; @@ -1352,6 +1364,8 @@ B8DC376025D06C2F00452A83 /* OpponentDeadForTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpponentDeadForTracker.swift; sourceTree = ""; }; B8E76170293A321A0048E802 /* FindGameState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindGameState.swift; sourceTree = ""; }; B8E76172293A3D010048E802 /* QueueEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueEvents.swift; sourceTree = ""; }; + B8E868822B93AF04005B1315 /* SceneHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneHandler.swift; sourceTree = ""; }; + B8E868842B940EB7005B1315 /* HearthDbConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HearthDbConverter.swift; sourceTree = ""; }; B8EF1D7B291681EA00AC4658 /* BattlegroundsQuestView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BattlegroundsQuestView.xib; sourceTree = ""; }; B8EF1D7D2916857000AC4658 /* BattlegroundsQuestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BattlegroundsQuestView.swift; sourceTree = ""; }; B8FE1F9228693EDD004C397C /* WotogCounter.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WotogCounter.xib; sourceTree = ""; }; @@ -1716,13 +1730,13 @@ isa = PBXGroup; children = ( 436F5D0B1CFD5F6A00DEB9A1 /* CardBar.swift */, - 436F5D0D1CFD64A300DEB9A1 /* ThemeElementInfo.swift */, + 439DD8F51DB2324A004EBE47 /* CardSize.swift */, 436F5D0F1CFD79B500DEB9A1 /* ClassicBar.swift */, 436F5D111CFD7E7500DEB9A1 /* DarkBar.swift */, 436F5D131CFD803C00DEB9A1 /* FrostBar.swift */, 436F5D151CFD822600DEB9A1 /* MinimalBar.swift */, - 439DD8F51DB2324A004EBE47 /* CardSize.swift */, 439DD8F91DB236A4004EBE47 /* ThemeElement.swift */, + 436F5D0D1CFD64A300DEB9A1 /* ThemeElementInfo.swift */, ); path = Cards; sourceTree = ""; @@ -1906,6 +1920,7 @@ 439C7EB31C777F8900D29859 /* Player.swift */, 439DD8F31DB23212004EBE47 /* PlayerTurn.swift */, B8E76172293A3D010048E802 /* QueueEvents.swift */, + B8E868822B93AF04005B1315 /* SceneHandler.swift */, 43E77EE31C94A19700C26950 /* TurnTimer.swift */, ); path = Logging; @@ -2059,11 +2074,12 @@ 43B695551C90653A00F4D0AC /* Hearthstone */ = { isa = PBXGroup; children = ( - 43B695561C90655C00F4D0AC /* Secrets */, + B8AB59992744145B00383EC8 /* Collection.swift */, + B8AB59972744139700383EC8 /* CollectionHelper.swift */, + B8E868842B940EB7005B1315 /* HearthDbConverter.swift */, F7D7A3A52011B67D00E3A5F0 /* Hearthstone.swift */, B8C33FB0273EF851006DA79E /* MercenariesCoins.swift */, - B8AB59972744139700383EC8 /* CollectionHelper.swift */, - B8AB59992744145B00383EC8 /* Collection.swift */, + 43B695561C90655C00F4D0AC /* Secrets */, ); path = Hearthstone; sourceTree = ""; @@ -2252,6 +2268,9 @@ children = ( B8AF0C722B8439A800B65F63 /* ConstructedMulliganGuide.swift */, B8AF0C6E2B8433DE00B65F63 /* ConstructedMulliganGuide.xib */, + B8AF0C802B92C5E800B65F63 /* ConstructedMulliganGuidePreLobby.swift */, + B8AF0C7A2B928AA400B65F63 /* ConstructedMulliganGuidePreLobby.xib */, + B8AF0C782B91444200B65F63 /* ConstructedMulliganGuidePreLobbyViewModel.swift */, B8AF0C702B84347800B65F63 /* ConstructedMulliganGuideViewModel.swift */, B8AF0C642B828A4A00B65F63 /* ConstructedMulliganSingleCardHeader.swift */, B895864C2B82614D00989C13 /* ConstructedMulliganSingleCardHeader.xib */, @@ -2259,6 +2278,8 @@ B8AF0C742B843C6C00B65F63 /* ConstructedMulliganSingleCardStats.swift */, B8AF0C6C2B83AAAE00B65F63 /* ConstructedMulliganSingleCardStats.xib */, B89586462B81400A00989C13 /* ConstructedMulliganSingleCardViewModel.swift */, + B8AF0C7E2B92B76700B65F63 /* ConstructedMulliganSingleDeckStatus.swift */, + B8AF0C7C2B9295A000B65F63 /* ConstructedMulliganSingleDeckStatus.xib */, B89586442B804E2000989C13 /* ConstructedStatsHeaderViewModel.swift */, ); path = Mulligan; @@ -2585,6 +2606,7 @@ 432B5FC21CB5373D00B05D19 /* SaveDeck.xib in Resources */, B864AE3A25A4CC1200793F36 /* ImportingPreferences.xib in Resources */, B8A70C152841494D004D054C /* BattlegroundsTierTriples.xib in Resources */, + B8AF0C7B2B928AA400B65F63 /* ConstructedMulliganGuidePreLobby.xib in Resources */, 437B5BB81CD2559300DE0A41 /* OpponentTrackersPreferences.xib in Resources */, 4335543A1D5FAF0300277B27 /* HSReplayPreferences.xib in Resources */, B83CF362294BB4E1003C9062 /* BattlegroundsQuestPicking.xib in Resources */, @@ -2593,6 +2615,7 @@ 4C3E2D94245F16B900292349 /* CollectionToastViewController.xib in Resources */, B8CE65D62950980100E8D168 /* BattlegroundsPlacementDistribution.xib in Resources */, B800FCA42732AA7C00AFBD51 /* MercenariesTaskListButton.xib in Resources */, + B8AF0C7D2B9295A000B65F63 /* ConstructedMulliganSingleDeckStatus.xib in Resources */, 432B5FB61CB5372600B05D19 /* GamePreferences.xib in Resources */, 439C7EE41C777FA900D29859 /* Tracker.xib in Resources */, 43873CC11CA6E6C200F01CB4 /* WindowMove.xib in Resources */, @@ -2850,10 +2873,12 @@ 43F204131E93C2E600E0BC4A /* Warrior.swift in Sources */, 43F2040B1E93C2E600E0BC4A /* Mage.swift in Sources */, B8FE1F992875BD15004C397C /* SynchronizedArray.swift in Sources */, + B8AF0C812B92C5E800B65F63 /* ConstructedMulliganGuidePreLobby.swift in Sources */, B812BCED2947F929000C6513 /* BattlegroundsQuestStatsParams.swift in Sources */, B800FCA62732AA9100AFBD51 /* MercenariesTaskListButton.swift in Sources */, B844D36A2A0C3497001704E0 /* SpellCardEntityProxy.swift in Sources */, B8DC120024EC5F9A0088A755 /* BobsBuddyPanel.swift in Sources */, + B8AF0C792B91444200B65F63 /* ConstructedMulliganGuidePreLobbyViewModel.swift in Sources */, 43F204121E93C2E600E0BC4A /* Warlock.swift in Sources */, B83CF364294BB808003C9062 /* BattlegroundsQuestPicking.swift in Sources */, B8A9CB332571F160000071FC /* GuessedCardState.swift in Sources */, @@ -2888,6 +2913,7 @@ B8D75B9125A9DCDE00446923 /* ExperienceOverlay.swift in Sources */, B8D34D35294FB0C100CACC5A /* BattlegroundsHeroHeaderViewModel.swift in Sources */, B804FC2429418CC200F74CD9 /* OverlayMessageViewModel.swift in Sources */, + B8E868852B940EB7005B1315 /* HearthDbConverter.swift in Sources */, 437B5BAC1CD1E6AF00DE0A41 /* Step.swift in Sources */, B8AC9A2A293F9B120047F438 /* BattlegroundsQuestStats.swift in Sources */, B8591FF62B23D98800B907A4 /* SendChoicesHandler.swift in Sources */, @@ -2898,6 +2924,7 @@ 43EECB961C7DE3BA0077AC1B /* DeckCellView.swift in Sources */, B804FC2829429ED900F74CD9 /* Tier7AllTime.swift in Sources */, 43A8A0F11E0BC08F00AF34FD /* JadeCounter.swift in Sources */, + B8E868832B93AF04005B1315 /* SceneHandler.swift in Sources */, B8AB59942742F94900383EC8 /* RateLimiter.swift in Sources */, 438B04F91E6F55720071594B /* Date.swift in Sources */, 437C58301EBC7E8400FFD888 /* NSImage.swift in Sources */, @@ -3014,6 +3041,7 @@ B8AF0C752B843C6C00B65F63 /* ConstructedMulliganSingleCardStats.swift in Sources */, 439C7EBB1C777F8900D29859 /* Mulligan.swift in Sources */, B844D36E2A0C35A8001704E0 /* MinionCardEntityProxy.swift in Sources */, + B8AF0C7F2B92B76700B65F63 /* ConstructedMulliganSingleDeckStatus.swift in Sources */, 4C19239D2392FA7E007EBB0F /* BattlegroundsDetailsWindow.swift in Sources */, B8AF0C672B83A65E00B65F63 /* MulliganGuideStatusData.swift in Sources */, B8DC120524EC79370088A755 /* OutputProxy.swift in Sources */, diff --git a/HSTracker/Assets/Assets.xcassets/mulligan-guide-data.imageset/Contents.json b/HSTracker/Assets/Assets.xcassets/mulligan-guide-data.imageset/Contents.json new file mode 100644 index 00000000..a4c40292 --- /dev/null +++ b/HSTracker/Assets/Assets.xcassets/mulligan-guide-data.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "mulligan-guide-data.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HSTracker/Assets/Assets.xcassets/mulligan-guide-data.imageset/mulligan-guide-data.png b/HSTracker/Assets/Assets.xcassets/mulligan-guide-data.imageset/mulligan-guide-data.png new file mode 100644 index 0000000000000000000000000000000000000000..74aea50a01f6a6bfb8c79643c9638db88965ce04 GIT binary patch literal 3754 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 U@DV9*RsaA107*qoM6N<$f>R0dBLDyZ literal 0 HcmV?d00001 diff --git a/HSTracker/Assets/Assets.xcassets/mulligan-guide-no-data.imageset/Contents.json b/HSTracker/Assets/Assets.xcassets/mulligan-guide-no-data.imageset/Contents.json new file mode 100644 index 00000000..6f5b015a --- /dev/null +++ b/HSTracker/Assets/Assets.xcassets/mulligan-guide-no-data.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "mulligan-guide-no-data.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/HSTracker/Assets/Assets.xcassets/mulligan-guide-no-data.imageset/mulligan-guide-no-data.png b/HSTracker/Assets/Assets.xcassets/mulligan-guide-no-data.imageset/mulligan-guide-no-data.png new file mode 100644 index 0000000000000000000000000000000000000000..ff65f60e4cd49e7e5296f61b13a0ad8e40369ee1 GIT binary patch literal 3720 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 Int { let h = (dict["kCGWindowBounds"] as? NSDictionary)?["Height"] as? Int ?? 0 let w = (dict["kCGWindowBounds"] as? NSDictionary)?["Width"] as? Int ?? 0 - + return w * h } func reload() { let options = CGWindowListOption(arrayLiteral: .excludeDesktopElements) let windowListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0)) - + if let info = (windowListInfo as? [NSDictionary])?.filter({dict in dict["kCGWindowOwnerName"] as? String == CoreManager.applicationName && dict["kCGWindowLayer"] as? Int == 0 }).sorted(by: { @@ -47,12 +47,12 @@ struct SizeHelper { if let id = info["kCGWindowNumber"] as? Int { self.windowId = CGWindowID(id) } - + let pid = info["kCGWindowOwnerPID"] as? pid_t ?? 0 let appRef = AXUIElementCreateApplication(pid) var window: CFTypeRef? - + let result: AXError = AXUIElementCopyAttributeValue(appRef, kAXFocusedWindowAttribute as CFString, &window) var calculateFromFrame = false if result == .success { @@ -85,7 +85,7 @@ struct SizeHelper { screenRect = screen.frame frame.origin.y = screen.frame.maxY - rect.maxY } - + //logger.debug("HS Frame is : \(rect)") self._frame = frame @@ -174,13 +174,13 @@ struct SizeHelper { return NSImage(cgImage: image, size: NSSize(width: image.width, - height: image.height)) + height: image.height)) } return nil } } - + static let hearthstoneWindow = HearthstoneWindow() static var battlegroundsTileHeight: CGFloat { @@ -206,14 +206,14 @@ struct SizeHelper { static var mercenariesMinionMargin: CGFloat { return hearthstoneWindow.width * screenRatio * 0.01 } - + static var minionMargin: CGFloat { return hearthstoneWindow.width * screenRatio * 0.0029 } - + static func overHearthstoneFrame() -> NSRect { let frame = hearthstoneWindow.frame - + return hearthstoneWindow.relativeFrame(frame) } @@ -240,12 +240,12 @@ struct SizeHelper { height: max(100, hearthstoneWindow.frame.height - offset - yOffset)) return hearthstoneWindow.relativeFrame(frame, relative: false) } - + static func getScaledXPos(_ left: CGFloat, width: CGFloat, ratio: CGFloat) -> CGFloat { return ((width) * ratio * left) + (width * (1 - ratio) / 2) } - + static func searchLocation() -> NSPoint { let hsRect = hearthstoneWindow.frame let ratio = (4.0 / 3.0) / (hsRect.width / hsRect.height) @@ -253,14 +253,14 @@ struct SizeHelper { let exportSearchBoxY: CGFloat = 0.915 var loc = NSPoint(x: getScaledXPos(exportSearchBoxX, width: hsRect.width, ratio: ratio), y: exportSearchBoxY * hsRect.height) - + // correct location with window origin. loc.x += hsRect.origin.x loc.y += ( hearthstoneWindow.screenRect.height - hsRect.origin.y - hsRect.size.height) return loc } - + static func firstCardFrame() -> NSRect { let location = firstCardLocation() return NSRect(x: location.x - 100, @@ -268,35 +268,35 @@ struct SizeHelper { width: 300, height: 100) } - + static func firstCardLocation() -> NSPoint { let hsRect = hearthstoneWindow.frame let ratio = (4.0 / 3.0) / (hsRect.width / hsRect.height) let cardPosOffset: CGFloat = 50 let exportCard1X: CGFloat = 0.04 let exportCard1Y: CGFloat = 0.168 - + let cardPosX = getScaledXPos(exportCard1X, width: hsRect.width, ratio: ratio) let cardPosY = exportCard1Y * hsRect.height var loc = NSPoint(x: cardPosX + cardPosOffset, y: cardPosY + cardPosOffset) - + // correct location with window origin. loc.x += hsRect.origin.x loc.y += (hearthstoneWindow.screenRect.height - hsRect.origin.y - hsRect.size.height) return loc } - + static func secondCardLocation() -> NSPoint { var loc = firstCardLocation() - + loc.x += 190 return loc } - + static func playerTrackerFrame() -> NSRect { return trackerFrame(xOffset: hearthstoneWindow.frame.width - trackerWidth) } - + static func opponentTrackerFrame() -> NSRect { var yOffset: CGFloat = 0 if Settings.preventOpponentNameCovering { @@ -320,19 +320,19 @@ struct SizeHelper { //NSRect(x: hearthstoneWindow.frame.maxX - 400, y: hearthstoneWindow.frame.origin.y, width: 150.0, height: 45.0) return hearthstoneWindow.relativeFrame(frame, keepRatio: true) } - + static func arenaHelperFrame() -> NSRect { let height: CGFloat = 450 let frame = NSRect(x: 0, y: (hearthstoneWindow.frame.height / 2) - (height / 2), width: trackerWidth, height: height) - + return hearthstoneWindow.relativeFrame(frame) } - + static func secretTrackerFrame(height: CGFloat) -> NSRect { let yOffset: CGFloat = hearthstoneWindow.isFullscreen() ? 0 : 50 - + let frame = NSRect(x: trackerWidth + 25, y: hearthstoneWindow.frame.height - height - yOffset, width: trackerWidth, @@ -340,7 +340,7 @@ struct SizeHelper { return hearthstoneWindow.relativeFrame(frame, relative: false) } - + static func timerHudFrame() -> NSRect { let frame = NSRect(x: 999.0, y: 423.0, width: 160.0, height: 115.0) return hearthstoneWindow.relativeFrame(frame) @@ -356,22 +356,22 @@ struct SizeHelper { static func battlegroundsSessionFrame() -> NSRect { let bottom = hearthstoneWindow.frame.minY + 0.05 * hearthstoneWindow.height let top = hearthstoneWindow.frame.maxY - 0.15 * hearthstoneWindow.height - + let left = hearthstoneWindow.frame.minX - + let frame = NSRect(x: left, y: bottom, width: 200, height: top - bottom) return (frame) } - + static func battlegroundsOverlayFrame() -> NSRect { let top = hearthstoneWindow.frame.minY + 0.85 * hearthstoneWindow.height let bottom = hearthstoneWindow.frame.minY + 0.15 * hearthstoneWindow.height - + // Looks like the HS board ratio is 1.5, the rest is padding let boardWidth = hearthstoneWindow.height * 1.5 let left = hearthstoneWindow.frame.minX + 0.05 * boardWidth + (hearthstoneWindow.width - boardWidth)/2 let right = hearthstoneWindow.frame.minX + 0.133 * boardWidth + (hearthstoneWindow.width - boardWidth)/2 - + let frame = NSRect(x: left, y: bottom, width: right - left, height: top - bottom) return (frame) } @@ -383,19 +383,19 @@ struct SizeHelper { let height = CGFloat(56) let width = CGFloat(tiers * 48 + 8) let x = hearthstoneWindow.frame.minX + hearthstoneWindow.frame.width - width - + return NSRect(x: x, y: trackerFrame.minY + trackerFrame.height - height, width: width, height: height) } - + static func bobsPanelOverlayFrame() -> NSRect { let trackerFrame = playerTrackerFrame() let height = CGFloat(52) let width = CGFloat(404) let x = hearthstoneWindow.frame.minX + (hearthstoneWindow.width - width) / 2 - + return NSRect(x: x, y: trackerFrame.minY + trackerFrame.height - height, width: width, height: height) } - + static func battlegroundsTierDetailFrame() -> NSRect { let height = hearthstoneWindow.height - CGFloat(64) let width = trackerWidth @@ -404,7 +404,7 @@ struct SizeHelper { return NSRect(x: x, y: y, width: width, height: height) } - + static func battlegroundsDetailsFrame() -> NSRect { let w: CGFloat = 1184 let h: CGFloat = 270 @@ -414,7 +414,7 @@ struct SizeHelper { let frame = NSRect(x: x, y: hearthstoneWindow.frame.maxY - h, width: w, height: h) return frame } - + static func toastFrame() -> NSRect { let w: CGFloat = 240.0 let h: CGFloat = 100.0 @@ -446,7 +446,7 @@ struct SizeHelper { let result = NSRect(x: frame.minX, y: frame.minY + height - (height / 2 - overlayHeight - opponentBoardOffset) - overlayHeight, width: width, height: overlayHeight + abilitySize() + margin) return result } - + static func playerBoardOverlay() -> NSRect { let width = hearthstoneWindow.width let height = hearthstoneWindow.height @@ -455,7 +455,7 @@ struct SizeHelper { let step = game.gameEntity?[.step] ?? 0 let isMainAction = step == Step.main_action.rawValue || step == Step.main_post_action.rawValue || step == Step.main_pre_action.rawValue let mercsToNominate = game.gameEntity?.has(tag: .allow_move_minion) ?? false - + let overlayHeight = boardOverlayHeight() let margin = overlayHeight * 0.14 let playerBoardOffset = game.isMercenariesMatch() ? isMainAction && !mercsToNominate ? height * -0.09 : height * 0.003 : height * 0.03 @@ -480,7 +480,7 @@ struct SizeHelper { let frame = NSRect(x: right - w, y: bottom, width: w, height: h) return frame } - + static func mercenariesTaskListView() -> NSRect { let frame = mercenariesTaskListButton() let height = hearthstoneWindow.height @@ -492,7 +492,7 @@ struct SizeHelper { static func flavorTextFrame() -> NSRect { let hs = hearthstoneWindow.frame - + let ft = AppDelegate.instance().coreManager.game.windowManager.flavorText.window?.frame ?? NSRect.zero let w = ft.width let h = ft.height @@ -517,7 +517,7 @@ struct SizeHelper { let h = 50.0 return NSRect(x: hs.minX + getScaledXPos(86.5 / 100.0, width: hs.width, ratio: screenRatio) - (w - 77.0), y: hs.minY + (hs.height * (100.0 - 18.5) / 100.0) - h, width: w, height: h) } - + static func playerWotogIconsFrame() -> NSRect { let hs = hearthstoneWindow.frame let w = 736.0 + 150.0 @@ -531,4 +531,11 @@ struct SizeHelper { let h = 500.0 return NSRect(x: hs.minX + getScaledXPos(0.079, width: hs.width, ratio: screenRatio), y: hs.minY + (hs.height * (1.0 - 0.103) - h), width: w, height: h) } + + static func constructedMulliganGuidePreLobbyFrame() -> NSRect { + let hs = SizeHelper.hearthstoneWindow.frame + let w = 238.0*3.0 + let h = 224.0*3.0 + return NSRect(x: hs.minX + SizeHelper.getScaledXPos(0.087, width: SizeHelper.hearthstoneWindow.width, ratio: SizeHelper.screenRatio), y: hs.minY + (hs.height * (1.0 - 0.217)) - h, width: w, height: h) + } } diff --git a/HSTracker/Database/Models/Deck.swift b/HSTracker/Database/Models/Deck.swift index a0a165a0..89b03614 100644 --- a/HSTracker/Database/Models/Deck.swift +++ b/HSTracker/Database/Models/Deck.swift @@ -152,6 +152,19 @@ class Deck: Object { return sortedCards.all { CardSet.twistSets().contains($0.set ?? .invalid) && !$0.isNeutral() } } + func guessFormatType() -> FormatType { + if isClassicDeck { + return .ft_classic + } + if isTwistDeck { + return .ft_twist + } + if isWildDeck { + return .ft_wild + } + return .ft_standard + } + /** * Compares the card content to the other deck */ diff --git a/HSTracker/HSReplay/HSReplay.swift b/HSTracker/HSReplay/HSReplay.swift index a61e1cd6..3be29abc 100644 --- a/HSTracker/HSReplay/HSReplay.swift +++ b/HSTracker/HSReplay/HSReplay.swift @@ -40,4 +40,5 @@ struct HSReplay { static let tier7AllTimeMMR = "\(baseApiUrl)/battlegrounds/alltime/" static let playerTrial = "\(baseApiUrl)/playertrials/" static let constructedMulliganGuide = "\(baseApiUrl)/mulligan/overlay/" + static let constructedMulliganGuideStatus = "\(baseApiUrl)/mulligan/status/" } diff --git a/HSTracker/HSReplay/HSReplayAPI.swift b/HSTracker/HSReplay/HSReplayAPI.swift index d548df94..f60788d3 100644 --- a/HSTracker/HSReplay/HSReplayAPI.swift +++ b/HSTracker/HSReplay/HSReplayAPI.swift @@ -658,4 +658,37 @@ class HSReplayAPI { } } + @available(macOS 10.15.0, *) + static func getMulliganGuideStatus(parameters: MulliganGuideStatusParams) async -> MulliganGuideStatusData? { + return await withCheckedContinuation { continuation in + let encoder = JSONEncoder() + var body: Data? + do { + body = try encoder.encode(parameters) + if let body = body { + logger.debug("Sending mulligan guide status request: \(String(data: body, encoding: .utf8) ?? "ERROR")") + } + } catch { + logger.error(error) + } + + oauthswift.client.request("\(HSReplay.constructedMulliganGuideStatus)", method: .POST, parameters: [:], headers: ["Content-Type": "application/json"], body: body, completionHandler: { result in + switch result { + case .success(let response): + if let str = String(data: response.data, encoding: .utf8) { + logger.debug("Response data: \(str)") + let bqs: MulliganGuideStatusData? = parseResponse(data: response.data, defaultValue: nil) + continuation.resume(returning: bqs) + } else { + continuation.resume(returning: nil) + } + return + case .failure(let error): + logger.error(error) + continuation.resume(returning: nil) + return + } + }) + } + } } diff --git a/HSTracker/HearthMirror-version.txt b/HSTracker/HearthMirror-version.txt index 6a256392..60b80996 100644 --- a/HSTracker/HearthMirror-version.txt +++ b/HSTracker/HearthMirror-version.txt @@ -1 +1 @@ -a81452486864290df9b72a183b88b25943e36e53 +509e96f0948a3c1743584fbf0df445090b985879 diff --git a/HSTracker/HearthMirror/MirrorHelper.swift b/HSTracker/HearthMirror/MirrorHelper.swift index ccee2479..65d0fe78 100644 --- a/HSTracker/HearthMirror/MirrorHelper.swift +++ b/HSTracker/HearthMirror/MirrorHelper.swift @@ -431,11 +431,19 @@ struct MirrorHelper { } static func getDeckPickerDecksOnPage() -> [MirrorCollectionDeckBoxVisual?] { - var result: [MirrorCollectionDeckBoxVisual?]? - MirrorHelper.accessQueue.sync { - result = mirror?.getDeckPickerDecksOnPage() + var result = [MirrorCollectionDeckBoxVisual?]() + MirrorHelper.accessQueue.sync { + if let data = mirror?.getDeckPickerDecksOnPage() { + for x in data { + if x is NSNull { + result.append(nil) + } else if let cbv = x as? MirrorCollectionDeckBoxVisual { + result.append(cbv) + } + } + } } - return result ?? [MirrorCollectionDeckBoxVisual?]() + return result } static func getDeckPickerState() -> MirrorDeckPickerState? { diff --git a/HSTracker/Hearthstone/HearthDbConverter.swift b/HSTracker/Hearthstone/HearthDbConverter.swift new file mode 100644 index 00000000..b020bf13 --- /dev/null +++ b/HSTracker/Hearthstone/HearthDbConverter.swift @@ -0,0 +1,45 @@ +// +// HearthDbConverter.swift +// HSTracker +// +// Created by Francisco Moraes on 3/2/24. +// Copyright © 2024 Benjamin Michotte. All rights reserved. +// + +import Foundation + +class HearthDbConverter { + static func toHearthDbDeck(deck: MirrorDeck, format: FormatType) -> DeckSerializer.Deck? { + if let card = Cards.hero(byId: deck.hero), card.dbfId > 0 { + let result = DeckSerializer.Deck() + result.name = deck.name + result.format = format + result.heroDbfId = card.dbfId + result.cards = deck.cards.compactMap { x in + let card = Cards.any(byId: x.cardId) + card?.count = x.count.intValue + return card + } + // TODO: sideboards + return result + } + return nil + } + + static func toHearthDbDeck(deck: Deck) -> DeckSerializer.Deck? { + if let card = Cards.hero(byId: deck.heroId), card.dbfId > 0 { + let result = DeckSerializer.Deck() + result.name = deck.name + result.format = deck.guessFormatType() + result.heroDbfId = card.dbfId + result.cards = deck.cards.compactMap { x in + let card = Cards.any(byId: x.id) + card?.count = x.count + return card + } + // TODO: sideboards + return result + } + return nil + } +} diff --git a/HSTracker/Importers/ClipboardImporter.swift b/HSTracker/Importers/ClipboardImporter.swift index fa352127..f2fd67b3 100644 --- a/HSTracker/Importers/ClipboardImporter.swift +++ b/HSTracker/Importers/ClipboardImporter.swift @@ -9,7 +9,7 @@ import Foundation class ClipboardImporter { - static func clipboardImport() -> DeckSerializer.SerializedDeck? { + static func clipboardImport() -> DeckSerializer.Deck? { if let string = NSPasteboard.general.string(forType: .string) { return DeckSerializer.deserialize(input: string) } diff --git a/HSTracker/Importers/DeckSerializer.swift b/HSTracker/Importers/DeckSerializer.swift index 8cdda230..a1f87cd3 100644 --- a/HSTracker/Importers/DeckSerializer.swift +++ b/HSTracker/Importers/DeckSerializer.swift @@ -12,21 +12,27 @@ class DeckSerializer { enum DeckSerializerError: Error { case argumentError } - - struct SerializedDeck { - let name: String - let playerClass: CardClass - let cards: [Card] + + class Deck { + var heroDbfId = 0 + var cards = [Card]() + var format = FormatType.ft_unknown + var name = "" + var deckId = Int64(0) + + func getHero() -> Card? { + return Cards.by(dbfId: heroDbfId, collectible: false) + } } - class func deserialize(input: String) -> SerializedDeck? { + static func deserialize(input: String) -> Deck? { let lines = input.components(separatedBy: .newlines).map { $0.trim() } + var deck: Deck? var deckName: String? - var playerClass: CardClass? - var cards: [Card]? + var deckId: String? for line in lines { if line.isBlank { continue } @@ -35,34 +41,32 @@ class DeckSerializer { if line.hasPrefix("###") { deckName = line.substring(from: 3).trim() } + if line.hasPrefix("# Deck ID:") { + deckId = line.substring(from: 10).trim() + } continue } - if let (_cardClass, _cards) = deserializeDeckString(deckString: line) { - playerClass = _cardClass - cards = _cards + if deck == nil { + deck = deserializeDeckString(deckString: line) } } - if deckName == nil { - deckName = "Imported Deck" + if let deck { + deck.name = deckName ?? "Imported Deck" + deck.deckId = Int64(deckId ?? "0") ?? 0 } - - guard let _deckName = deckName, - let _playerClass = playerClass, - let _cards = cards else { return nil } - - return SerializedDeck(name: _deckName, - playerClass: _playerClass, - cards: _cards) + return deck } - class func deserializeDeckString(deckString: String) -> (CardClass, [Card])? { + static func deserializeDeckString(deckString: String) -> Deck? { guard let data = Data(base64Encoded: deckString) else { logger.error("Can not decode \(deckString)") return nil } + + let deck = Deck() let bytes = [UInt8](data) @@ -85,7 +89,11 @@ class DeckSerializer { _ = try? read() // Format - determined dynamically - _ = try? read() + guard let format = try? FormatType(rawValue: Int(read().toInt64())) else { + logger.error("cannot get format") + return nil + } + deck.format = format // Num Heroes - always 1 _ = try? read() @@ -94,12 +102,7 @@ class DeckSerializer { logger.error("Can not get heroId") return nil } - guard let heroCard = Cards.by(dbfId: Int(heroId.toInt64()), collectible: false) else { - logger.error("Can not get heroCard") - return nil - } - let cardClass = heroCard.playerClass - logger.verbose("Got class \(cardClass)") + deck.heroDbfId = Int(heroId.toInt64()) var cards: [Card] = [] func addCard(dbfId: Varint? = nil, count: Int = 1) { @@ -136,17 +139,49 @@ class DeckSerializer { addCard(dbfId: dbfId, count: count) } - return (cardClass, cards) + return deck } - - class func serialize(deck: Deck) -> String? { - guard let hero = Cards.hero(byId: deck.heroId) - ?? Cards.hero(byPlayerClass: deck.playerClass) else { - logger.error("Deck has no hero") - return nil + + static func serialize(deck: Deck, includeComments: Bool) -> String? { + guard let deckString = serialize(deck: deck) else { + return nil } + if !includeComments { + return deckString + } + let hero = "\(deck.getHero()?.playerClass ?? .invalid)".capitalized + var sb = "### \(deck.name.isEmpty ? hero + " Deck" : deck.name)\n" + sb.append("# Class: \(hero)\n") + sb.append("# Format: \("\(deck.format)".substring(from: 3).capitalized)\n") + sb.append("#\n") + for card in deck.cards.sortCardList() { + sb.append("# \(card.count)x (\(card.cost) \(card.name)") + // TODO: sideboards + } + sb.append("#\n") + sb.append("\(deckString)\n") + sb.append("#\n") + sb.append("# To use this deck, copy it to your clipboard and create a new deck in Hearthstone\n") + return sb + } - let heroDbfId = hero.dbfId + static func serialize(deck: Deck?) -> String? { + guard let deck else { + logger.debug("Deck can not be null") + return nil + } + guard deck.heroDbfId != 0 else { + logger.debug("HeroDbfId can not be zero") + return nil + } + guard deck.getHero()?.type == .hero else { + logger.debug("HeroDbfId does not represent a valid hero") + return nil + } + guard deck.format != .ft_unknown else { + logger.debug("Format can not be FT_UNKNOWN") + return nil + } var data = Data() func write(value: Int) { @@ -156,10 +191,10 @@ class DeckSerializer { data.append(contentsOf: [0]) write(value: 1) - write(value: deck.isWildDeck ? 1 : 2) + write(value: deck.format.rawValue) write(value: 1) - write(value: heroDbfId) - let cards = deck.sortedCards.sorted(by: { + write(value: deck.heroDbfId) + let cards = deck.cards.sorted(by: { return $0.dbfId < $1.dbfId }) let singleCards = cards.filter({ $0.count == 1 }) @@ -181,6 +216,8 @@ class DeckSerializer { write(value: card.dbfId) write(value: card.count) } + + // TODO: sideboards return data.base64EncodedString() } diff --git a/HSTracker/Logging/CoreManager.swift b/HSTracker/Logging/CoreManager.swift index 54a8f330..78dd11ec 100644 --- a/HSTracker/Logging/CoreManager.swift +++ b/HSTracker/Logging/CoreManager.swift @@ -79,6 +79,13 @@ final class CoreManager: NSObject { self.game.showTier7PreLobby(show: !args.isAnyOpen(), checkAccountStatus: false) } } + DeckPickerWatcher.change = { _, args in + self.game.setDeckPickerState(args.selectedFormatType, args.decksOnPage, args.isModalOpen) + } + + SceneWatcher.change = { _, args in + SceneHandler.onSceneUpdate(prevMode: Mode.allCases[args.prevMode], mode: Mode.allCases[args.mode], sceneLoaded: args.sceneLoaded, transitioning: args.transitioning) + } ExperienceWatcher.newExperienceHandler = { args in AppDelegate.instance().coreManager.game.experienceChangedAsync(experience: args.experience, experienceNeeded: args.experienceNeeded, level: args.level, levelChange: args.levelChange, animate: args.animate) @@ -306,6 +313,8 @@ final class CoreManager: NSObject { ExperienceWatcher.stop() QueueWatcher.stop() BaconWatcher.stop() + SceneWatcher.stop() + DeckPickerWatcher.stop() MirrorHelper.destroy() game.windowManager.battlegroundsHeroPicking.viewModel.reset() game.windowManager.battlegroundsQuestPicking.viewModel.reset() @@ -316,6 +325,7 @@ final class CoreManager: NSObject { // MARK: - Events func startListeners() { + SceneWatcher.start() if self.triggers.count == 0 { let center = NSWorkspace.shared.notificationCenter let notifications = [ diff --git a/HSTracker/Logging/Enums/Mode.swift b/HSTracker/Logging/Enums/Mode.swift index 91afba9c..bafc6df7 100644 --- a/HSTracker/Logging/Enums/Mode.swift +++ b/HSTracker/Logging/Enums/Mode.swift @@ -8,7 +8,7 @@ import Foundation -enum Mode: String { +enum Mode: String, CaseIterable { case invalid, startup, login, diff --git a/HSTracker/Logging/Game.swift b/HSTracker/Logging/Game.swift index dc54fb48..236b8b01 100644 --- a/HSTracker/Logging/Game.swift +++ b/HSTracker/Logging/Game.swift @@ -580,6 +580,17 @@ class Game: NSObject, PowerEventHandler { self.windowManager.show(controller: self.windowManager.constructedMulliganGuide, show: false) } } + + if self.windowManager.constructedMulliganGuidePreLobby.isVisible { + if hsActive && Settings.showMulliganGuidePreLobby { + self.windowManager.show(controller: self.windowManager.constructedMulliganGuidePreLobby, show: true, frame: SizeHelper.constructedMulliganGuidePreLobbyFrame(), overlay: true) + DispatchQueue.main.async { + self.windowManager.constructedMulliganGuidePreLobby.updateScaling() + } + } else { + self.windowManager.show(controller: self.windowManager.constructedMulliganGuidePreLobby, show: false) + } + } } } @@ -1551,7 +1562,8 @@ class Game: NSObject, PowerEventHandler { let playerClass = deck.playerClass let heroId = deck.heroId let isArena = deck.isArena - let shortid = DeckSerializer.serialize(deck: deck) + + let shortid = DeckSerializer.serialize(deck: HearthDbConverter.toHearthDbDeck(deck: deck)) DispatchQueue.main.async { cards = cards.sortCardList() self.currentDeck = PlayingDeck(id: deckId, @@ -1758,6 +1770,10 @@ class Game: NSObject, PowerEventHandler { turnTimer.stop() isInMenu = true + + DispatchQueue.main.async { + self.updateMulliganGuidePreLobby() + } } private func generateEndgameStatistics() -> InternalGameStats? { @@ -1912,12 +1928,17 @@ class Game: NSObject, PowerEventHandler { } if isConstructedMatch() { // Core.Overlay.HideMulliganToast(false); - player.mulliganCardStats = nil DispatchQueue.main.async { + self.player.mulliganCardStats = nil self.hideMulliganGuideStats() } } + // TODO: add +// if isConstructedMatch() || isFriendlyMatch || isArenaMatch { +// capturemMulliganGuideFeedback() +// } + if let currentDeck = self.currentDeck { var skip = false if previousMode == Mode.adventure { @@ -2118,14 +2139,16 @@ class Game: NSObject, PowerEventHandler { if player == .player && !isInMenu { // Clear some state that should never be active at the start of a turn in case another hiding mechanism fails DispatchQueue.main.async { + // Clear some state that should never be active at the start of a turn in case another hiding mechanism fails self.hideMulliganGuideStats() + self.player.mulliganCardStats = nil + + self.windowManager.battlegroundsHeroPicking.viewModel.reset() + self.windowManager.show(controller: self.windowManager.battlegroundsHeroPicking, show: false) } - self.player.mulliganCardStats = nil if isBattlegroundsMatch() { DispatchQueue.main.async { [self] in - self.windowManager.battlegroundsHeroPicking.viewModel.reset() - self.windowManager.show(controller: self.windowManager.battlegroundsHeroPicking, show: false) OpponentDeadForTracker.shoppingStarted(game: self) if playerTurn.turn > 1 { BobsBuddyInvoker.instance(gameId: self.gameId, turn: self.turnNumber() - 1)?.startShopping() @@ -2193,7 +2216,7 @@ class Game: NSObject, PowerEventHandler { } func isConstructedMatch() -> Bool { - return currentGameType == .gt_ranked || currentGameType == .gt_casual || currentGameType == .gt_vs_friend || currentGameType == .gt_vs_ai + return currentGameType == .gt_ranked || currentGameType == .gt_casual || currentGameType == .gt_vs_friend /*|| currentGameType == .gt_vs_ai */ } func isMulliganDone() -> Bool { @@ -3246,6 +3269,48 @@ class Game: NSObject, PowerEventHandler { return MulliganGuideParams(deckstring: activeDeck.shortid, game_type: BnetGameType.getBnetGameType(gameType: currentGameType, format: currentFormat).rawValue, format_type: currentFormatType.rawValue, opponent_class: opponentClass?.rawValue.uppercased() ?? CardClass.invalid.rawValue.uppercased(), player_initiative: player.hasCoin ? "COIN" : "FIRST", player_star_level: starMedal > 0 ? starMedal : 1, player_region: Region.toBnetRegion(region: currentRegion)) } + + @MainActor + private func showMulliganGuidePreLobby() { + windowManager.constructedMulliganGuidePreLobby.isVisible = true + windowManager.show(controller: windowManager.constructedMulliganGuidePreLobby, show: true) + } + + @MainActor + private func hideMulliganGuidePreLobby() { + windowManager.constructedMulliganGuidePreLobby.isVisible = false + windowManager.show(controller: windowManager.constructedMulliganGuidePreLobby, show: false) + } + + @MainActor + func updateMulliganGuidePreLobby() { + let isPremium = HSReplayAPI.accountData?.is_premium ?? false + + let show = isInMenu && SceneHandler.scene == .tournament && Settings.enableMulliganGuide && Settings.showMulliganGuidePreLobby && isPremium + if show { + showMulliganGuidePreLobby() + if #available(macOS 10.15.0, *) { + Task.detached { [self] in + await windowManager.constructedMulliganGuidePreLobby.viewModel.ensureLoaded() + } + } + } else { + hideMulliganGuidePreLobby() + } + } + + func setDeckPickerState(_ vft: VisualsFormatType, _ decksList: [CollectionDeckBoxVisual?], _ isModalOpen: Bool) { + let vm = windowManager.constructedMulliganGuidePreLobby.viewModel + if vm.decksOnPage == nil || decksList != vm.decksOnPage { + vm.decksOnPage = decksList + } + vm.visualsFormatType = vft + vm.isModalOpen = isModalOpen + } + + func setConstructedQueue(_ inQueue: Bool) { + windowManager.constructedMulliganGuidePreLobby.viewModel.isInQueue = inQueue + } } // MARK: NSWindowDelegate functions diff --git a/HSTracker/Logging/Handlers/LoadingScreenHandler.swift b/HSTracker/Logging/Handlers/LoadingScreenHandler.swift index 8dbf7990..50024b90 100644 --- a/HSTracker/Logging/Handlers/LoadingScreenHandler.swift +++ b/HSTracker/Logging/Handlers/LoadingScreenHandler.swift @@ -124,23 +124,6 @@ struct LoadingScreenHandler: LogEventParser { game.cacheBrawlInfo() } - if game.currentMode == .bacon { - game.cacheBattlegroundRatingInfo() - game.showBattlegroundsSession(true, true) - if #available(macOS 10.15, *) { - game.showTier7PreLobby(show: true, checkAccountStatus: true) - BaconWatcher.start() - } - } else { - if game.currentMode != .gameplay { - game.showBattlegroundsSession(false, true) - } - if #available(macOS 10.15, *) { - game.showTier7PreLobby(show: false, checkAccountStatus: false) - BaconWatcher.stop() - } - } - if game.currentMode == .lettuce_play { game.cacheMercenariesRatingInfo() } diff --git a/HSTracker/Logging/QueueEvents.swift b/HSTracker/Logging/QueueEvents.swift index 211fca82..2a843b29 100644 --- a/HSTracker/Logging/QueueEvents.swift +++ b/HSTracker/Logging/QueueEvents.swift @@ -25,6 +25,9 @@ class QueueEvents { if !QueueEvents.modes.contains(_game.currentMode ?? Mode.invalid) && !QueueEvents.lettuceModes.contains(_game.currentMode ?? Mode.invalid) { return } + if _game.currentMode == .tournament { + _game.setConstructedQueue(e.isInQueue) + } if e.isInQueue { // _game.metadata.enqueueTime = Date.now() diff --git a/HSTracker/Logging/SceneHandler.swift b/HSTracker/Logging/SceneHandler.swift new file mode 100644 index 00000000..57754b61 --- /dev/null +++ b/HSTracker/Logging/SceneHandler.swift @@ -0,0 +1,79 @@ +// +// SceneHandler.swift +// HSTracker +// +// Created by Francisco Moraes on 3/2/24. +// Copyright © 2024 Benjamin Michotte. All rights reserved. +// + +import Foundation + +class SceneHandler { + static private(set) var scene: Mode? + + static private var transitioning: Bool? + + static func onSceneUpdate(prevMode: Mode, mode: Mode, sceneLoaded: Bool, transitioning: Bool) { + if SceneHandler.transitioning == nil || transitioning { + onSceneTransitionStart(from: prevMode, to: mode) + SceneHandler.transitioning = true + } + if !transitioning && sceneLoaded { + onSceneTransitionComplete(from: prevMode, to: mode) + SceneHandler.transitioning = false + } + } + + private static func onSceneTransitionStart(from: Mode, to: Mode) { + SceneHandler.scene = nil + + guard let core = AppDelegate.instance().coreManager else { + return + } + let game = core.game + + if from == .tournament { + + DispatchQueue.main.async { + game.updateMulliganGuidePreLobby() + } + DeckPickerWatcher.stop() + game.windowManager.constructedMulliganGuidePreLobby.viewModel.invlidateAllDecks() + } + + if from == .bacon { + if to != .gameplay { + game.showBattlegroundsSession(false, true) + } + if #available(macOS 10.15, *) { + game.showTier7PreLobby(show: false, checkAccountStatus: false) + BaconWatcher.stop() + } + } + } + + private static func onSceneTransitionComplete(from: Mode, to: Mode) { + SceneHandler.scene = to + + guard let core = AppDelegate.instance().coreManager else { + return + } + let game = core.game + + if to == .tournament { + DeckPickerWatcher.start() + DispatchQueue.main.async { + game.updateMulliganGuidePreLobby() + } + } + if to == .bacon { + game.cacheBattlegroundRatingInfo() + + game.showBattlegroundsSession(true, true) + if #available(macOS 10.15, *) { + game.showTier7PreLobby(show: true, checkAccountStatus: true) + BaconWatcher.start() + } + } + } +} diff --git a/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobby.swift b/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobby.swift new file mode 100644 index 00000000..cd10805c --- /dev/null +++ b/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobby.swift @@ -0,0 +1,118 @@ +// +// ConstructedMulliganGuide.swift +// HSTracker +// +// Created by Francisco Moraes on 2/19/24. +// Copyright © 2024 Benjamin Michotte. All rights reserved. +// + +import Foundation + +class ConstructedMulliganGuidePreLobby: OverWindowController { + @IBOutlet weak var stack1: NSStackView! + @IBOutlet weak var stack2: NSStackView! + @IBOutlet weak var stack3: NSStackView! + @IBOutlet weak var outerView: NSView! + @IBOutlet weak var scaleView: NSView! + + let viewModel = ConstructedMulliganGuidePreLobbyViewModel() + + var isVisible = false + private var deferred = false + + override init(window: NSWindow?) { + super.init(window: window) + + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + commonInit() + } + + func commonInit() { + viewModel.propertyChanged = { name in + DispatchQueue.main.async { + self.update(name) + } + } + } + + override func awakeFromNib() { + } + + func updateScaling() { + guard let window else { + logger.debug("Missing window") + return + } + let rect = SizeHelper.constructedMulliganGuidePreLobbyFrame() + let bounds = NSRect(x: 0, y: 0, width: rect.width, height: rect.height) + logger.debug("bounds: \(bounds)") + let scale = SizeHelper.hearthstoneWindow.height / 1080 + let sw = bounds.width * scale + let sh = bounds.height * scale + scaleView.frame = NSRect(x: 0, y: window.frame.height - sh - 16 * scale, width: sw, height: sh) + logger.debug("scaleView frame: \(scaleView.frame)") + scaleView.bounds = bounds + logger.debug("scaleView frame: \(scaleView.frame)") + scaleView.needsDisplay = true + } + + func update(_ property: String?) { + let all = property == nil + + logger.debug("\(#function) - property \(property ?? "nil")") + if property == "pageStatusRows" || all { + if let stack1, let stack2, let stack3 { + for old in stack1.arrangedSubviews { + old.removeFromSuperview() + } + for old in stack2.arrangedSubviews { + old.removeFromSuperview() + } + for old in stack3.arrangedSubviews { + old.removeFromSuperview() + } + + let rows = viewModel.pageStatusRows + var rowIndex = 0 + for row in rows { + for status in row { + let view = ConstructedMulliganSingleDeckStatus(frame: NSRect(x: 0, y: 0, width: 238, height: 96), status: status) + switch rowIndex { + case 0: + stack1.addArrangedSubview(view) + case 1: + stack2.addArrangedSubview(view) + case 2: + stack3.addArrangedSubview(view) + default: + continue + } + } + rowIndex += 1 + } +// if viewModel.visibility { +// let rect = SizeHelper.constructedMulliganGuidePreLobbyFrame() +// AppDelegate.instance().coreManager.game.windowManager.show(controller: self, show: true, frame: rect, overlay: true) +// updateScaling() +// isVisible = true +// if deferred { +// DispatchQueue.main.async { +// self.update(nil) +// self.updateScaling() +// } +// } +// } else if isVisible { +// AppDelegate.instance().coreManager.game.windowManager.show(controller: self, show: false) +// isVisible = false +// } + } else { + deferred = true + } + } + } +} diff --git a/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobby.xib b/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobby.xib new file mode 100644 index 00000000..b69d948a --- /dev/null +++ b/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobby.xib @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobbyViewModel.swift b/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobbyViewModel.swift new file mode 100644 index 00000000..346be658 --- /dev/null +++ b/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganGuidePreLobbyViewModel.swift @@ -0,0 +1,391 @@ +// +// ConstructedMulliganGuidePreLobbyViewModel.swift +// HSTracker +// +// Created by Francisco Moraes on 2/29/24. +// Copyright © 2024 Benjamin Michotte. All rights reserved. +// + +import Foundation + +enum SingleDeckState { + case invalid, + loading, // indicates that a task is currently fetching some + no_data, + ready +} + +class SingleDeckStatus { + private(set) var visibility: Bool + private(set) var state: SingleDeckState + private(set) var cardClass: CardClass + private(set) var isFocused: Bool + var padding: Int { + return cardClass == .deathknight ? 29 : 15 + } + + init() { + visibility = false + state = .invalid + cardClass = .invalid + isFocused = false + } + + init(state: SingleDeckState, cardClass: CardClass, isFocused: Bool) { + self.visibility = true + self.state = state + self.cardClass = cardClass + self.isFocused = isFocused + } + + var iconVisibility: Bool { + return switch state { + case .ready, .no_data, .loading: + true + default: + false + } + } + + var iconSource: NSImage? { + return switch state { + case .no_data: + NSImage(named: "mulligan-guide-no-data") + default: + NSImage(named: "mulligan-guide-data") + } + } + + var borderBrush: String { + return switch state { + case .no_data: + "#CCE3D000" + default: + "#CC00AA00" + } + } + + var background: String { + return switch state { + case .no_data: + "#CC1A1100" + default: + "#CC002200" + } + } + + var label: String { + return switch state { + case .loading: + String.localizedString("ConstructedMulliganGuidePreLobby_Status_Loading", comment: "") + case .no_data: + String.localizedString("ConstructedMulliganGuidePreLobby_Status_NoData", comment: "") + case .ready: + String.localizedString("ConstructedMulliganGuidePreLobby_Status_Ready", comment: "") + default: + "\(state)" + } + } + + var labelVisibility: Bool { + return isFocused + } +} + +class ConstructedMulliganGuidePreLobbyViewModel: ViewModel { + private var _deckStatusByDeckstring = [BnetGameType: [String: SingleDeckState]]() + + override init() { + // TODO: HSReplayNetOAuth.AccountDataUpdated += () => Core.Overlay.UpdateMulliganGuidePreLobby(); + // TODO: HSReplayNetOAuth.LoggedOut += () => Core.Overlay.UpdateMulliganGuidePreLobby(); + } + + // MARK: - Pagination + var decksOnPage: [CollectionDeckBoxVisual?]? { + get { + return getProp(nil) + } + set { + setProp(newValue) + onPropertyChanged("pageStatus") + onPropertyChanged("pageStatusRows") + onPropertyChanged("validDecksOnPage") + } + } + + var validDecksOnPage: [CollectionDeckBoxVisual?]? { + return decksOnPage?.map { x in + guard let x else { + return nil + } + return !x.isShowingInvalidCardCount ? x : nil + } + } + + // MARK: - Deckstrings + + struct DeckData { + var cardClass: CardClass + var deckstring: String + } + + private var _decksByFormatAndDeckId = [FormatType: [Int64: DeckData]]() + + private static func isElligibleForFormat(deck: MirrorDeck, formatType: FormatType) -> Bool { + let deckFormat = FormatType(rawValue: deck.formatType.intValue) ?? FormatType.ft_unknown + return switch formatType { + case .ft_standard: + deckFormat == .ft_standard + case .ft_wild: + deckFormat == .ft_standard || deckFormat == .ft_wild + case .ft_classic: + deckFormat == .ft_classic + case .ft_twist: + deckFormat == .ft_twist + default: + false + } + } + + private static func getDeckDataByDeckId(formatType: FormatType) -> [Int64: DeckData] { + var cache = [Int64: DeckData]() + + guard let decks = MirrorHelper.getDecks() else { + return cache + } + for deck in decks { + if !isElligibleForFormat(deck: deck, formatType: formatType) { + continue + } + + guard let hearthDbDeck = HearthDbConverter.toHearthDbDeck(deck: deck, format: formatType) else { + continue + } + let deckData = DeckData(cardClass: hearthDbDeck.getHero()?.playerClass ?? .invalid, deckstring: DeckSerializer.serialize(deck: hearthDbDeck) ?? "") + cache[deck.id.int64Value] = deckData + } + return cache + } + + private func cacheDecks(formatType: FormatType) -> [Int64: DeckData] { + let cache = ConstructedMulliganGuidePreLobbyViewModel.getDeckDataByDeckId(formatType: formatType) + _decksByFormatAndDeckId[formatType] = cache + return cache + } + + // MARK: - VisualsFormatType + + var visualsFormatType: VisualsFormatType { + get { + return getProp(.vft_unknown) + } + set { + setProp(newValue) + onPropertyChanged("gameType") + onPropertyChanged("formatType") + onPropertyChanged("pageStatus") + onPropertyChanged("pageStatusRows") + if #available(macOS 10.15.0, *) { + Task.detached { + await self.ensureLoaded() + } + } + } + } + + private var gameType: BnetGameType { + return switch visualsFormatType { + case .vft_standard: + BnetGameType.bgt_ranked_standard + case .vft_wild: + BnetGameType.bgt_ranked_wild + case .vft_twist: + BnetGameType.bgt_ranked_twist + case .vft_casual: + BnetGameType.bgt_casual_wild + default: + BnetGameType.bgt_unknown + } + } + + var formatType: FormatType { + return switch visualsFormatType { + case .vft_standard: + FormatType.ft_standard + case .vft_wild: + FormatType.ft_wild + case .vft_twist: + FormatType.ft_twist + case .vft_casual: + FormatType.ft_wild + default: + FormatType.ft_unknown + } + } + + // MARK: - Visibility + var isModalOpen: Bool { + get { + return getProp(false) + } + set { + setProp(newValue) + onPropertyChanged("visibility") + } + } + + var isInQueue: Bool { + get { + return getProp(false) + } + set { + setProp(newValue) + onPropertyChanged("visibility") + } + } + + var visibility: Bool { + return isModalOpen || isInQueue ? false : true + } + + // MARK: - + + @available(macOS 10.15.0, *) + private static func loadMulliganGuideStatus(gameType: BnetGameType, starLevel: Int?, deckstrings: [String]) async -> [String: MulliganGuideStatusData.Status]? { + if deckstrings.count == 0 { + return [String: MulliganGuideStatusData.Status]() + } + + let parameters = MulliganGuideStatusParams(decks: deckstrings, game_type: gameType.rawValue, star_level: starLevel) + guard let result = await HSReplayAPI.getMulliganGuideStatus(parameters: parameters) else { + return nil + } + return Dictionary(uniqueKeysWithValues: deckstrings.compactMap { x in + if let res = result.decks[x] { + return (x, MulliganGuideStatusData.Status(rawValue: res.status) ?? .NO_DATA) + } else { + return (x, MulliganGuideStatusData.Status.NO_DATA) + } + }) + } + + @available(macOS 10.15.0, *) + func ensureLoaded() async { + await update(true) + await update() + } + + @available(macOS 10.15.0, *) + private func update(_ onlyVisibilePage: Bool = false) async { + if gameType == .bgt_unknown || formatType == .ft_unknown { + return + } + + // Generate the deckstrings for the current format + + let deckboxes = _decksByFormatAndDeckId[formatType].map { x in x } ?? cacheDecks(formatType: formatType) + + // Assemble the deck strings that are not known yet + if _deckStatusByDeckstring[gameType] == nil { + _deckStatusByDeckstring[gameType] = [String: SingleDeckState]() + } + var toLoad = [String]() + if onlyVisibilePage { + guard let validDecksOnPage else { + return + } + for box in validDecksOnPage { + guard let box else { + continue + } + if let deckData = deckboxes[box.deckid], _deckStatusByDeckstring[gameType]?[deckData.deckstring] == nil { + toLoad.append(deckData.deckstring) + _deckStatusByDeckstring[gameType]?[deckData.deckstring] = .loading + } + } + } else { + for deckbox in deckboxes.values where _deckStatusByDeckstring[gameType]?[deckbox.deckstring] == nil { + toLoad.append(deckbox.deckstring) + _deckStatusByDeckstring[gameType]?[deckbox.deckstring] = .loading + } + } + + onPropertyChanged("pageStatus") + onPropertyChanged("pageStatusRows") + + // Assemble the request + if toLoad.count > 0 { + let medalInfo = MirrorHelper.getMedalData() + var starLevel: Int? + if let medalInfo { + let medalInfoData: MirrorMedalInfo? = switch visualsFormatType { + case .vft_standard: + medalInfo.standard + case .vft_wild: + medalInfo.wild + case .vft_classic: + medalInfo.classic + case .vft_twist: + medalInfo.twist + default: + nil + } + starLevel = medalInfoData?.starLevel.intValue + } + // It's important to copy this out, because it can change while awaiting the mulligan guide status + // => this would lead to a "miscache" + let theGameType = gameType + let results = await ConstructedMulliganGuidePreLobbyViewModel.loadMulliganGuideStatus(gameType: theGameType, starLevel: starLevel, deckstrings: toLoad) ?? [String: MulliganGuideStatusData.Status]() + + for result in results { + _deckStatusByDeckstring[theGameType]?[result.key] = switch result.value { + case .READY: + SingleDeckState.ready + default: + SingleDeckState.no_data + } + } + + onPropertyChanged("pageStatus") + onPropertyChanged("pageStatusRows") + } + } + + var pageStatus: [SingleDeckStatus] { + guard let validDecksOnPage, formatType != .ft_unknown, let deckMap = _decksByFormatAndDeckId[formatType], let allDecks = _deckStatusByDeckstring[gameType] else { + return [SingleDeckStatus]() + } + return validDecksOnPage.compactMap { x in + if let box = x, let deckData = deckMap[box.deckid] { + // At this point we know the deck is valid for this format, so either fetch the API status or show NO_DATA + if let state = allDecks[deckData.deckstring] { + return SingleDeckStatus(state: state, cardClass: deckData.cardClass, isFocused: box.isFocused || box.isSelected) + } + return SingleDeckStatus(state: .no_data, cardClass: deckData.cardClass, isFocused: box.isSelected) + } + return SingleDeckStatus() + } + } + + // PageStatus, but grouped into 3 rows of 3 cols + var pageStatusRows: [[SingleDeckStatus]] { + + return pageStatus.chunks(3) + } + + func invalidateDeck(deckId: Int64) { + // Clear from deckId -> deckstring mapping + for formatType in _decksByFormatAndDeckId.keys { + _decksByFormatAndDeckId[formatType]?.removeValue(forKey: deckId) + } + } + + func invlidateAllDecks() { + _decksByFormatAndDeckId.removeAll() + } + + func reset() { + _decksByFormatAndDeckId.removeAll() + _deckStatusByDeckstring.removeAll() + } +} diff --git a/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganSingleDeckStatus.swift b/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganSingleDeckStatus.swift new file mode 100644 index 00000000..c671c5a9 --- /dev/null +++ b/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganSingleDeckStatus.swift @@ -0,0 +1,64 @@ +// +// ConstructedMulliganSingleCardStats.swift +// HSTracker +// +// Created by Francisco Moraes on 2/19/24. +// Copyright © 2024 Benjamin Michotte. All rights reserved. +// + +import Foundation + +class ConstructedMulliganSingleDeckStatus: NSView { + @IBOutlet weak var contentView: NSView! + + @IBOutlet weak var padding: NSLayoutConstraint! + + @IBOutlet weak var box: NSBox! + + let status: SingleDeckStatus + + @objc dynamic var label: String { + return status.label + } + + @objc dynamic var labelVisibility: Bool { + return status.labelVisibility + } + + @objc dynamic var iconVisibility: Bool { + return status.iconVisibility + } + + @objc dynamic var iconSource: NSImage? { + return status.iconSource + } + + override var intrinsicContentSize: NSSize { + return NSSize(width: 238.0, height: 96.0) + } + + init(frame: NSRect, status: SingleDeckStatus) { + self.status = status + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + fatalError("Not implemented") + } + + private func commonInit() { + Bundle.main.loadNibNamed("ConstructedMulliganSingleDeckStatus", owner: self, topLevelObjects: nil) + translatesAutoresizingMaskIntoConstraints = true + contentView.translatesAutoresizingMaskIntoConstraints = true + addSubview(contentView) + + } + + override func awakeFromNib() { + padding.constant = CGFloat(status.padding) + box.isHidden = !status.visibility + box.borderColor = NSColor.fromHexString(hex: status.borderBrush) ?? .black + box.fillColor = NSColor.fromHexString(hex: status.background) ?? .clear + } +} diff --git a/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganSingleDeckStatus.xib b/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganSingleDeckStatus.xib new file mode 100644 index 00000000..5da16598 --- /dev/null +++ b/HSTracker/UIs/Constructed/Mulligan/ConstructedMulliganSingleDeckStatus.xib @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HSTracker/UIs/DeckManager/DeckManager.swift b/HSTracker/UIs/DeckManager/DeckManager.swift index 242ea7b2..56dd68c1 100644 --- a/HSTracker/UIs/DeckManager/DeckManager.swift +++ b/HSTracker/UIs/DeckManager/DeckManager.swift @@ -550,7 +550,7 @@ class DeckManager: NSWindowController { @IBAction func exportHSString(_ sender: Any?) { guard let deck = currentDeck else { return } - guard let string = DeckSerializer.serialize(deck: deck) else { + guard let string = DeckSerializer.serialize(deck: HearthDbConverter.toHearthDbDeck(deck: deck)) else { NSAlert.show(style: .critical, message: String.localizedString("Can't create deck string.", comment: ""), window: self.window!) diff --git a/HSTracker/UIs/DeckManager/NewDeck.swift b/HSTracker/UIs/DeckManager/NewDeck.swift index d3061c90..1dbc2415 100644 --- a/HSTracker/UIs/DeckManager/NewDeck.swift +++ b/HSTracker/UIs/DeckManager/NewDeck.swift @@ -85,7 +85,7 @@ class NewDeck: NSWindowController, NSControlTextEditingDelegate { if let serializedDeck = ClipboardImporter.clipboardImport() { let deck = Deck() let cards: [Card]? - deck.playerClass = serializedDeck.playerClass + deck.playerClass = serializedDeck.getHero()?.playerClass ?? .invalid deck.name = serializedDeck.name cards = serializedDeck.cards diff --git a/HSTracker/UIs/Preferences/Base.lproj/TrackersPreferences.xib b/HSTracker/UIs/Preferences/Base.lproj/TrackersPreferences.xib index 26aebad1..0295b01c 100644 --- a/HSTracker/UIs/Preferences/Base.lproj/TrackersPreferences.xib +++ b/HSTracker/UIs/Preferences/Base.lproj/TrackersPreferences.xib @@ -23,6 +23,7 @@ + @@ -34,11 +35,11 @@ - + - + @@ -114,7 +115,7 @@ - + @@ -159,7 +160,7 @@ - + @@ -253,7 +254,7 @@ +