From ebec50cb8ab0b6bb80672c1b7d1e39ca39c8ead8 Mon Sep 17 00:00:00 2001 From: Mateus Cechetto Date: Wed, 23 Oct 2024 15:51:43 -0300 Subject: [PATCH] feat: Show related cards (#41) * wip: fizzle snapshot tracking * wip: add link between copied card and the original * wip: work for multiple snapshots * wip: player's tidepool pupil tracking * wip: display related cards on overlay and hydration station * wip: show related cards grid * wip: show related cards when hovering in hand * wip: scale grid depending on maxHeight - useful because when hovering cards in hand we need to make the grid smaller so the highlighted card doesnt cover it * wip: hover on hand work for deck on the left * fix: tidepool pupil considering non spell cards * fix: display card grid position when scaling window * fix: clear related cards when card is shuffled into the deck * feat: show related cards on hover in hand * wip: add related cards label * wip: add cards implementations * fix: add zindex to cardgridtooltip * fix: grid positioning * feat: add pet parrot * chore: add related cards tooltip to the configs * refactor: create functions to set related cards tooltip Also: - adjusts indentation - adds comment for fizzle snapshot * fix: update VMActionTests * feat: update tyr to only show unique cards * feat: localize tooltip label * refactor: move GetRelatedCards to a new class * refactor: create RelatedCardsSystem * refactor: create CardUtils * feat: implement cards with related cards * refactor: delete old related cards handler * fix: stop showing card on related cards box when it is already on player decklist * fix: tooltip on deckLens when the deck is big * fix: update MockGame * fix: use localized string * fix: Tyrs Tears normal and forged cardIds * fix: hover on hand for entity related cards * feat: add Product9 * feat: add Lady Liadrin * feat: add Shudderwock * fix: multiple entity related cards in hand --- HDTTests/Hearthstone/Secrets/MockGame.cs | 2 + .../ValueMoments/Actions/VMActionTests.cs | 4 +- Hearthstone Deck Tracker/Config.cs | 6 + .../Controls/GridCardImages.xaml.cs | 27 ++- Hearthstone Deck Tracker/Core.cs | 3 +- .../Options/Overlay/OverlayOpponent.xaml | 5 + .../Options/Overlay/OverlayOpponent.xaml.cs | 17 ++ .../Options/Overlay/OverlayPlayer.xaml | 5 + .../Options/Overlay/OverlayPlayer.xaml.cs | 17 ++ Hearthstone Deck Tracker/GameEventHandler.cs | 10 +- .../Hearthstone/CardUtils.cs | 44 +++++ .../Hearthstone/CounterSystem/BaseCounter.cs | 43 +---- .../Hearthstone/Entities/Entity.cs | 6 +- .../Hearthstone/GameV2.cs | 4 + Hearthstone Deck Tracker/Hearthstone/IGame.cs | 2 + .../Hearthstone/Player.cs | 69 +++++++- .../RelatedCardsSystem/Cards/DefaultCard.cs | 12 ++ .../Cards/DemonHunter/ReturnPolicy.cs | 22 +++ .../Cards/Druid/HydrationStation.cs | 24 +++ .../Cards/Hunter/PetParrot.cs | 23 +++ .../Cards/Hunter/Product9.cs | 23 +++ .../Cards/Hunter/Sasquawk.cs | 22 +++ .../Cards/Hunter/StranglethornHeart.cs | 22 +++ .../RelatedCardsSystem/Cards/Mage/Rewind.cs | 23 +++ .../Cards/Mage/TheGalacticProjectionOrb.cs | 23 +++ .../Cards/Paladin/LadyLiadrin.cs | 16 ++ .../Cards/Paladin/LadyLiadrinCore.cs | 20 +++ .../RelatedCardsSystem/Cards/Paladin/Tyr.cs | 23 +++ .../Cards/Paladin/TyrsTears.cs | 23 +++ .../Cards/Paladin/TyrsTearsForged.cs | 19 +++ .../Cards/Rogue/TessGreymane.cs | 21 +++ .../Cards/Rogue/TessGreymaneCore.cs | 22 +++ .../Cards/Shaman/Shudderwock.cs | 21 +++ .../Cards/Warrior/InventorBoom.cs | 23 +++ .../ICardWithRelatedCards.cs | 11 ++ .../RelatedCardsSystem/RelatedCardsManager.cs | 40 +++++ .../Hearthstone/Watchers.cs | 2 + Hearthstone Deck Tracker/IGameHandler.cs | 1 + .../LogReader/Handlers/PowerHandler.cs | 107 +++++++++++- .../LogReader/Handlers/TagChangeActions.cs | 7 + .../LogReader/HsGameState.cs | 2 +- .../LogReader/Interfaces/IHsGameState.cs | 2 +- .../HotKeys/PredefinedHotKeyActions.cs | 12 ++ .../Actions/Action/GeneralSettings.cs | 6 + .../Windows/OverlayWindow.DeckLists.cs | 11 +- .../Windows/OverlayWindow.Tooltips.cs | 161 +++++++++++++++++- .../Windows/OverlayWindow.Update.cs | 2 +- .../Windows/OverlayWindow.xaml | 3 +- .../Windows/OverlayWindow.xaml.cs | 2 +- 49 files changed, 948 insertions(+), 67 deletions(-) create mode 100644 Hearthstone Deck Tracker/Hearthstone/CardUtils.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/DefaultCard.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/DemonHunter/ReturnPolicy.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Druid/HydrationStation.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/PetParrot.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/Product9.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/Sasquawk.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/StranglethornHeart.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Mage/Rewind.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Mage/TheGalacticProjectionOrb.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/LadyLiadrin.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/LadyLiadrinCore.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/Tyr.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/TyrsTears.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/TyrsTearsForged.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Rogue/TessGreymane.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Rogue/TessGreymaneCore.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Shaman/Shudderwock.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Warrior/InventorBoom.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/ICardWithRelatedCards.cs create mode 100644 Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/RelatedCardsManager.cs diff --git a/HDTTests/Hearthstone/Secrets/MockGame.cs b/HDTTests/Hearthstone/Secrets/MockGame.cs index 692347ad6..4e79cbc7d 100644 --- a/HDTTests/Hearthstone/Secrets/MockGame.cs +++ b/HDTTests/Hearthstone/Secrets/MockGame.cs @@ -7,6 +7,7 @@ using Hearthstone_Deck_Tracker.Hearthstone; using Hearthstone_Deck_Tracker.Hearthstone.CounterSystem; using Hearthstone_Deck_Tracker.Hearthstone.Entities; +using Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem; using Hearthstone_Deck_Tracker.Hearthstone.Secrets; using Hearthstone_Deck_Tracker.Stats; using Card = Hearthstone_Deck_Tracker.Hearthstone.Card; @@ -28,6 +29,7 @@ public MockGame() public Entity PlayerEntity { get; set; } public Entity OpponentEntity { get; set; } public CounterManager CounterManager { get; set; } + public RelatedCardsManager RelatedCardsManager { get; set; } public bool IsMulliganDone { get; set; } public bool IsInMenu { get; set; } public bool IsUsingPremade { get; set; } diff --git a/HDTTests/Utility/ValueMoments/Actions/VMActionTests.cs b/HDTTests/Utility/ValueMoments/Actions/VMActionTests.cs index 0e9392551..288190013 100644 --- a/HDTTests/Utility/ValueMoments/Actions/VMActionTests.cs +++ b/HDTTests/Utility/ValueMoments/Actions/VMActionTests.cs @@ -83,7 +83,9 @@ public void VMAction_MixpanelPayloadReturnsCorrect() "player_wotog_counters", "opponent_wotog_counters", "player_counters", - "opponent_counters" + "opponent_counters", + "player_related_cards", + "opponent_related_cards" }}, { "hdt_general_settings_disabled", new []{ "overlay_hide_completely", diff --git a/Hearthstone Deck Tracker/Config.cs b/Hearthstone Deck Tracker/Config.cs index d81415246..feb3e3613 100644 --- a/Hearthstone Deck Tracker/Config.cs +++ b/Hearthstone Deck Tracker/Config.cs @@ -407,6 +407,9 @@ public class Config [DefaultValue(false)] public bool HideOpponentCounters = false; + [DefaultValue(false)] + public bool HideOpponentRelatedCards = false; + [DefaultValue(DisplayMode.Auto)] public DisplayMode OpponentCthunCounter = DisplayMode.Auto; @@ -467,6 +470,9 @@ public class Config [DefaultValue(false)] public bool HidePlayerCounters = false; + [DefaultValue(false)] + public bool HidePlayerRelatedCards = false; + [DefaultValue(true)] public bool DisablePlayerWotogs = true; diff --git a/Hearthstone Deck Tracker/Controls/GridCardImages.xaml.cs b/Hearthstone Deck Tracker/Controls/GridCardImages.xaml.cs index 1735b5288..70d6d5b99 100644 --- a/Hearthstone Deck Tracker/Controls/GridCardImages.xaml.cs +++ b/Hearthstone Deck Tracker/Controls/GridCardImages.xaml.cs @@ -77,10 +77,20 @@ public GridCardImages() } private IEnumerable? _previousCards; Storyboard? ExpandAnimation => FindResource("AnimateGrid") as Storyboard; - public async void SetCardIdsFromCards(IEnumerable? cards) + public async void SetCardIdsFromCards(IEnumerable? cards, int? maxGridHeight = null) { - if (cards == null || (_previousCards != null && _previousCards.SequenceEqual(cards))) + if(cards == null || (_previousCards != null && _previousCards.SequenceEqual(cards))) + { + if((maxGridHeight.HasValue && maxGridHeight != _maxGridHeight) + || (!maxGridHeight.HasValue && GridHeight != _maxGridHeight)) + { + _maxGridHeight = maxGridHeight ?? GridHeight; + CardsCollectionChanged(_maxGridHeight); + ExpandAnimation?.Begin(); + } + return; + } _previousCards = cards; @@ -122,7 +132,7 @@ public async void SetCardIdsFromCards(IEnumerable? cards) Cards.Add(cardWithImage); } - CardsCollectionChanged(); + CardsCollectionChanged(maxGridHeight); // Clear the loading image source after all cards are processed LoadingImageSource = null; @@ -132,7 +142,7 @@ public async void SetCardIdsFromCards(IEnumerable? cards) ExpandAnimation?.Begin(); } - private void CardsCollectionChanged() + private void CardsCollectionChanged(int? maxGridHeight = null) { var cardCount = Cards.Count; if (cardCount == 0) @@ -150,6 +160,13 @@ private void CardsCollectionChanged() else cardHeight = (int)(cardWidth / cardRatio); + if (maxGridHeight.HasValue && cardHeight * rows > maxGridHeight.Value) + { + var scaleFactor = (double)maxGridHeight.Value / (cardHeight * rows); + cardWidth = (int)(cardWidth * scaleFactor); + cardHeight = (int)(cardHeight * scaleFactor); + } + CardWidth = Math.Min(cardWidth, (int)MaxCardWidth); CardHeight = Math.Min(cardHeight, (int)MaxCardHeight); } @@ -165,6 +182,8 @@ public void SetTitle(string title) private const double MaxCardWidth = 256 * 0.75; private const double MaxCardHeight = 388 * 0.75; + + private int _maxGridHeight = GridHeight; public Thickness CardMargin => CalculateCardMargin(); private Thickness CalculateCardMargin() diff --git a/Hearthstone Deck Tracker/Core.cs b/Hearthstone Deck Tracker/Core.cs index dbcf49d5b..6784557f4 100644 --- a/Hearthstone Deck Tracker/Core.cs +++ b/Hearthstone Deck Tracker/Core.cs @@ -453,7 +453,8 @@ internal static async void UpdateOpponentCards(bool reset = false) _updateRequestsOpponent--; if(_updateRequestsOpponent > 0) return; - Overlay.UpdateOpponentCards(new List(Game.Opponent.OpponentCardList), reset); + var cardWithRelatedCards = Game.RelatedCardsManager.GetCardsOpponentMayHave(Game.Opponent).ToList(); + Overlay.UpdateOpponentCards(new List(Game.Opponent.OpponentCardList), cardWithRelatedCards, reset); if(Windows.OpponentWindow.IsVisible) Windows.OpponentWindow.UpdateOpponentCards(new List(Game.Opponent.OpponentCardList), reset); } diff --git a/Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayOpponent.xaml b/Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayOpponent.xaml index 64f0afd4c..a56139eb1 100644 --- a/Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayOpponent.xaml +++ b/Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayOpponent.xaml @@ -197,6 +197,11 @@ Margin="10,5,0,0" VerticalAlignment="Top" Checked="CheckBoxCounters_Checked" Unchecked="CheckBoxCounters_Unchecked" /> + (); ComboBoxCthun.SelectedItem = Config.Instance.OpponentCthunCounter; @@ -344,6 +345,22 @@ private void CheckBoxCounters_Unchecked(object sender, RoutedEventArgs e) Config.Save(); } + private void CheckBoxRelatedCards_Checked(object sender, RoutedEventArgs e) + { + if(!_initialized) + return; + Config.Instance.HideOpponentRelatedCards = false; + Config.Save(); + } + + private void CheckBoxRelatedCards_Unchecked(object sender, RoutedEventArgs e) + { + if(!_initialized) + return; + Config.Instance.HideOpponentRelatedCards = true; + Config.Save(); + } + private void CheckBoxWotogs_Checked(object sender, RoutedEventArgs e) { if(!_initialized) diff --git a/Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayPlayer.xaml b/Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayPlayer.xaml index 5d6f16c7d..5850293f1 100644 --- a/Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayPlayer.xaml +++ b/Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayPlayer.xaml @@ -161,6 +161,11 @@ Margin="10,5,0,0" VerticalAlignment="Top" Checked="CheckBoxCounters_Checked" Unchecked="CheckBoxCounters_Unchecked" /> + (); @@ -359,6 +360,22 @@ private void CheckBoxCounters_Unchecked(object sender, RoutedEventArgs e) Config.Save(); } + private void CheckBoxRelatedCards_Checked(object sender, RoutedEventArgs e) + { + if(!_initialized) + return; + Config.Instance.HidePlayerRelatedCards = false; + Config.Save(); + } + + private void CheckBoxRelatedCards_Unchecked(object sender, RoutedEventArgs e) + { + if(!_initialized) + return; + Config.Instance.HidePlayerRelatedCards = true; + Config.Save(); + } + private void CheckBoxWotogs_Checked(object sender, RoutedEventArgs e) { if(!_initialized) diff --git a/Hearthstone Deck Tracker/GameEventHandler.cs b/Hearthstone Deck Tracker/GameEventHandler.cs index b66b17a6c..548d9cb3c 100644 --- a/Hearthstone Deck Tracker/GameEventHandler.cs +++ b/Hearthstone Deck Tracker/GameEventHandler.cs @@ -1747,6 +1747,13 @@ public void HandlePlayerSecretPlayed(Entity entity, string cardId, int turn, Zon } } + public void HandlePlayerSecretTrigger(Entity entity, string? cardId, int turn, int otherId) + { + if (!entity.IsSecret) + return; + _game.Player.SecretTriggered(entity, turn); + } + public void HandlePlayerHandDiscard(Entity entity, string cardId, int turn) { if(string.IsNullOrEmpty(cardId)) @@ -2196,7 +2203,7 @@ public void HandleOpponentSecretTrigger(Entity entity, string? cardId, int turn, { if (!entity.IsSecret) return; - _game.Opponent.SecretTriggered(entity, turn); + _game.Opponent.OpponentSecretTriggered(entity, turn); _game.SecretsManager.RemoveSecret(entity); Core.UpdateOpponentCards(); var card = Database.GetCardFromId(cardId); @@ -2254,6 +2261,7 @@ public void HandleQuestRewardDatabaseId(int id, int value) void IGameHandler.HandlePlayerDraw(Entity entity, string cardId, int turn) => HandlePlayerDraw(entity, cardId, turn); void IGameHandler.HandlePlayerMulligan(Entity entity, string cardId) => HandlePlayerMulligan(entity, cardId); void IGameHandler.HandlePlayerSecretPlayed(Entity entity, string cardId, int turn, Zone fromZone, string parentBlockCardId) => HandlePlayerSecretPlayed(entity, cardId, turn, fromZone, parentBlockCardId); + void IGameHandler.HandlePlayerSecretTrigger(Entity entity, string? cardId, int turn, int otherId) => HandlePlayerSecretTrigger(entity, cardId, turn, otherId); void IGameHandler.HandlePlayerHandDiscard(Entity entity, string cardId, int turn) => HandlePlayerHandDiscard(entity, cardId, turn); void IGameHandler.HandlePlayerPlay(Entity entity, string cardId, int turn, string parentBlockCardId) => HandlePlayerPlay(entity, cardId, turn, parentBlockCardId); void IGameHandler.HandlePlayerDeckDiscard(Entity entity, string cardId, int turn) => HandlePlayerDeckDiscard(entity, cardId, turn); diff --git a/Hearthstone Deck Tracker/Hearthstone/CardUtils.cs b/Hearthstone Deck Tracker/Hearthstone/CardUtils.cs new file mode 100644 index 000000000..c4a13ded6 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/CardUtils.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using HearthDb.Enums; +using Hearthstone_Deck_Tracker.Enums; + +namespace Hearthstone_Deck_Tracker.Hearthstone; + +public static class CardUtils +{ + public static IEnumerable FilterCardsByFormat(this IEnumerable cards, Format? format) + { + return cards.Where(card => IsCardFromFormat(card, format)); + } + + public static bool IsCardFromFormat(Card? card, Format? format) + { + return format switch + { + Format.Classic => card != null && Helper.ClassicOnlySets.Contains(card.Set), + Format.Wild => card != null && !Helper.ClassicOnlySets.Contains(card.Set), + Format.Standard => card != null && !Helper.WildOnlySets.Contains(card.Set) && !Helper.ClassicOnlySets.Contains(card.Set), + Format.Twist => card != null && Helper.TwistSets.Contains(card.Set), + _ => true + }; + } + + public static IEnumerable FilterCardsByPlayerClass(this IEnumerable cards, string? playerClass, bool ignoreNeutral = false) + { + return cards.Where(card => IsCardFromPlayerClass(card, playerClass, ignoreNeutral)); + } + + public static bool IsCardFromPlayerClass(Card? card, string? playerClass, bool ignoreNeutral = false) + { + return card != null && + (card.PlayerClass == playerClass || card.GetTouristVisitClass() == playerClass || + (!ignoreNeutral && card.CardClass == CardClass.NEUTRAL)); + } + + public static bool MayCardBeRelevant(Card? card, Format? format, string? playerClass, + bool ignoreNeutral = false) + { + return IsCardFromFormat(card, format) && IsCardFromPlayerClass(card, playerClass, ignoreNeutral); + } +} diff --git a/Hearthstone Deck Tracker/Hearthstone/CounterSystem/BaseCounter.cs b/Hearthstone Deck Tracker/Hearthstone/CounterSystem/BaseCounter.cs index bfc8b3e14..10bb7598b 100644 --- a/Hearthstone Deck Tracker/Hearthstone/CounterSystem/BaseCounter.cs +++ b/Hearthstone Deck Tracker/Hearthstone/CounterSystem/BaseCounter.cs @@ -71,47 +71,14 @@ protected List GetCardsInDeckOrKnown(string[] cardIds) protected string[] FilterCardsByClassAndFormat(string[] cardIds, string? playerClass, bool ignoreNeutral = false) { - var filteredByFormat = FilterCardsByFormat(cardIds); - - var cardsToDisplay = filteredByFormat + return cardIds .Select(Database.GetCardFromId) - .Where(card => - card != null && - (card.PlayerClass == playerClass || (card.GetTouristVisitClass() == playerClass) || - !ignoreNeutral && card.CardClass == CardClass.NEUTRAL)) - .Select(card => card!.Id).ToArray(); - - return cardsToDisplay; + .FilterCardsByFormat(Game.CurrentFormat)! + .FilterCardsByPlayerClass(playerClass, ignoreNeutral) + .Select(card => card!.Id) + .ToArray(); } - private string[] FilterCardsByFormat(string[] cardIds) - { - switch(Game.CurrentFormat) - { - case Format.Classic: - return cardIds - .Select(Database.GetCardFromId) - .Where(card => card != null && Helper.ClassicOnlySets.Contains(card.Set)) - .Select(card => card!.Id).ToArray(); - case Format.Wild: - return cardIds - .Select(Database.GetCardFromId) - .Where(card => card != null && !Helper.ClassicOnlySets.Contains(card.Set)) - .Select(card => card!.Id).ToArray(); - case Format.Standard: - return cardIds - .Select(Database.GetCardFromId) - .Where(card => card != null && !Helper.WildOnlySets.Contains(card.Set) && !Helper.ClassicOnlySets.Contains(card.Set)) - .Select(card => card!.Id).ToArray(); - case Format.Twist: - return cardIds - .Select(Database.GetCardFromId) - .Where(card => card != null && Helper.TwistSets.Contains(card.Set)) - .Select(card => card!.Id).ToArray(); - default: - return cardIds; - } - } public event EventHandler? CounterChanged; diff --git a/Hearthstone Deck Tracker/Hearthstone/Entities/Entity.cs b/Hearthstone Deck Tracker/Hearthstone/Entities/Entity.cs index 1b78e909d..41bdcb2f1 100644 --- a/Hearthstone Deck Tracker/Hearthstone/Entities/Entity.cs +++ b/Hearthstone Deck Tracker/Hearthstone/Entities/Entity.cs @@ -279,7 +279,8 @@ public EntityInfo CloneWithNewEntity(Entity entity) GuessedCardState = GuessedCardState, LatestCardId = LatestCardId, StoredCardIds = StoredCardIds, - DeckIndex = DeckIndex + DeckIndex = DeckIndex, + CopyOfCardId = CopyOfCardId }; } @@ -355,6 +356,7 @@ public int GetCreatorId() public bool? OriginalEntityWasCreated { get; internal set; } public GuessedCardState GuessedCardState { get; set; } = GuessedCardState.None; public List StoredCardIds { get; set; } = new List(); + public string? CopyOfCardId { get; set; } public int DeckIndex { get; set; } public bool InGraveardAtStartOfGame { get; set; } @@ -400,6 +402,8 @@ public override string ToString() sb.Append(", deckIndex=" + DeckIndex); if(Forged) sb.Append(", forged=true"); + if(CopyOfCardId != null) + sb.Append(", copyOf=" + CopyOfCardId); return sb.ToString(); } } diff --git a/Hearthstone Deck Tracker/Hearthstone/GameV2.cs b/Hearthstone Deck Tracker/Hearthstone/GameV2.cs index 11db2fcd4..69cd54481 100644 --- a/Hearthstone Deck Tracker/Hearthstone/GameV2.cs +++ b/Hearthstone Deck Tracker/Hearthstone/GameV2.cs @@ -18,6 +18,7 @@ using Hearthstone_Deck_Tracker.Hearthstone.CounterSystem; using Hearthstone_Deck_Tracker.Hearthstone.EffectSystem; using Hearthstone_Deck_Tracker.Hearthstone.Entities; +using Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem; using Hearthstone_Deck_Tracker.Hearthstone.Secrets; using Hearthstone_Deck_Tracker.HsReplay; using Hearthstone_Deck_Tracker.Live; @@ -56,6 +57,7 @@ public class GameV2 : IGame public GameMetrics Metrics { get; private set; } = new(); public ActiveEffects ActiveEffects { get; } public CounterManager CounterManager { get; } + public RelatedCardsManager RelatedCardsManager { get; } public GameV2() { Player = new Player(this, true); @@ -64,6 +66,7 @@ public GameV2() SecretsManager = new SecretsManager(this, new RemoteArenaSettings()); ActiveEffects = new ActiveEffects(); CounterManager = new CounterManager(this); + RelatedCardsManager = new RelatedCardsManager(); _battlegroundsBoardState = new BattlegroundsBoardState(this); _battlegroundsHeroLatestTavernUpTurn = new Dictionary>(); _battlegroundsHeroTriplesByTier = new Dictionary>(); @@ -367,6 +370,7 @@ public void Reset(bool resetStats = true) Player.Reset(); Opponent.Reset(); ActiveEffects.Reset(); + RelatedCardsManager.Reset(); if(!_matchInfoCacheInvalid && MatchInfo?.LocalPlayer != null && MatchInfo.OpposingPlayer != null) UpdatePlayers(MatchInfo); ProposedAttacker = 0; diff --git a/Hearthstone Deck Tracker/Hearthstone/IGame.cs b/Hearthstone Deck Tracker/Hearthstone/IGame.cs index 28aff8af9..ca7e9dd0a 100644 --- a/Hearthstone Deck Tracker/Hearthstone/IGame.cs +++ b/Hearthstone Deck Tracker/Hearthstone/IGame.cs @@ -8,6 +8,7 @@ using Hearthstone_Deck_Tracker.Enums.Hearthstone; using Hearthstone_Deck_Tracker.Hearthstone.CounterSystem; using Hearthstone_Deck_Tracker.Hearthstone.Entities; +using Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem; using Hearthstone_Deck_Tracker.Hearthstone.Secrets; using Hearthstone_Deck_Tracker.Stats; @@ -23,6 +24,7 @@ public interface IGame Entity? PlayerEntity { get; } Entity? OpponentEntity { get; } CounterManager CounterManager { get; } + RelatedCardsManager RelatedCardsManager { get; } bool IsMulliganDone { get; } bool IsInMenu { get; set; } bool IsUsingPremade { get; set; } diff --git a/Hearthstone Deck Tracker/Hearthstone/Player.cs b/Hearthstone Deck Tracker/Hearthstone/Player.cs index 318a25a71..584145909 100644 --- a/Hearthstone Deck Tracker/Hearthstone/Player.cs +++ b/Hearthstone Deck Tracker/Hearthstone/Player.cs @@ -16,6 +16,7 @@ using Hearthstone_Deck_Tracker.Utility.Extensions; using HSReplay.Requests; using HSReplay.Responses; +using LiveCharts.Helpers; using static HearthDb.CardIds; #endregion @@ -37,15 +38,21 @@ public Player(IGame game, bool isLocalPlayer) public int Id { get; set; } public int Fatigue { get; set; } public bool IsLocalPlayer { get; } - public int SpellsPlayedCount { get; private set; } + public int SpellsPlayedCount => SpellsPlayedCardIds.Count; + public List SpellsPlayedCardIds { get; private set; } = new(); + public List SpellsPlayedInFriendlyCharacters { get; private set; } = new(); + public List CardsPlayedThisMatch { get; } = new(); public List CardsPlayedThisTurn { get; private set; } = new List(); + public List CardsPlayedLastTurn { get; private set; } = new(); public bool IsPlayingWhizbang { get; set; } public int PogoHopperPlayedCount { get; private set; } - public string? LastDiedMinionCardId { get; set; } + public string? LastDiedMinionCardId => DeadMinionsCardIds.LastOrDefault(); + public List DeadMinionsCardIds { get; } = new(); public string? LastDrawnCardId { get; set; } public int LibramReductionCount { get; private set; } public HashSet PlayedSpellSchools { get; private set; } = new HashSet(); public int AbyssalCurseCount { get; private set; } + public List SecretsTriggeredCardIds { get; } = new(); public bool HasCoin => Hand.Any(e => e.IsTheCoin); public int HandCount => Hand.Count(); @@ -457,14 +464,18 @@ public void Reset() Id = -1; Fatigue = 0; InDeckPredictions.Clear(); - SpellsPlayedCount = 0; + SpellsPlayedCardIds.Clear(); PogoHopperPlayedCount = 0; CardsPlayedThisTurn.Clear(); + CardsPlayedLastTurn.Clear(); + CardsPlayedThisMatch.Clear(); + SecretsTriggeredCardIds.Clear(); LastDrawnCardId = null; LibramReductionCount = 0; PlayedSpellSchools.Clear(); AbyssalCurseCount = 0; PastHeroPowers.Clear(); + DeadMinionsCardIds.Clear(); } public void Draw(Entity entity, int turn) @@ -509,7 +520,16 @@ public void Play(Entity entity, int turn) } break; case (int)CardType.SPELL: - SpellsPlayedCount++; + if(entity.CardId != null) + { + SpellsPlayedCardIds.Add(entity.CardId); + if(entity.HasTag(GameTag.CARD_TARGET) + && Core.Game.Entities.TryGetValue(entity.GetTag(GameTag.CARD_TARGET), out var target) + && target.IsControlledBy(Id)) + { + SpellsPlayedInFriendlyCharacters.Add(entity.CardId); + } + } if(entity.Tags.TryGetValue(GameTag.SPELL_SCHOOL, out var spellSchoolTag)) PlayedSpellSchools.Add((SpellSchool)spellSchoolTag); break; @@ -517,13 +537,21 @@ public void Play(Entity entity, int turn) entity.Info.Hidden = false; entity.Info.Turn = turn; entity.Info.CostReduction = 0; + if(entity.CardId != NonCollectible.Neutral.PhotographerFizzle_FizzlesSnapshotToken) + { + entity.Info.StoredCardIds.Clear(); + } if(entity.CardId != null) + { CardsPlayedThisTurn.Add(entity.CardId); + CardsPlayedThisMatch.Add(entity.CardId); + } //Log(entity); } public void OnTurnStart() { + CardsPlayedLastTurn = CardsPlayedThisTurn.ToList(); CardsPlayedThisTurn.Clear(); } @@ -609,6 +637,10 @@ public void HandToDeck(Entity entity, int turn) entity.Info.Returned = true; entity.Info.DrawerId = null; entity.Info.Hidden = true; + if(entity.CardId != NonCollectible.Neutral.PhotographerFizzle_FizzlesSnapshotToken) + { + entity.Info.StoredCardIds.Clear(); + } //Log(entity); } @@ -651,6 +683,13 @@ private void UpdateKnownEntitiesInDeck(string cardId, int turn = int.MaxValue) } public void SecretTriggered(Entity entity, int turn) + { + if(entity.CardId != null) + SecretsTriggeredCardIds.Add(entity.CardId); + //Log(entity); + } + + public void OpponentSecretTriggered(Entity entity, int turn) { entity.Info.Turn = turn; _game.SecretsManager.SecretTriggered(entity); @@ -668,7 +707,10 @@ public void SecretPlayedFromDeck(Entity entity, int turn) public void SecretPlayedFromHand(Entity entity, int turn) { entity.Info.Turn = turn; - SpellsPlayedCount++; + if(entity.CardId != null) + { + SpellsPlayedCardIds.Add(entity.CardId); + } if(entity.Tags.TryGetValue(GameTag.SPELL_SCHOOL, out var spellSchoolTag)) PlayedSpellSchools.Add((SpellSchool)spellSchoolTag); //Log(entity); @@ -677,14 +719,20 @@ public void SecretPlayedFromHand(Entity entity, int turn) public void QuestPlayedFromHand(Entity entity, int turn) { entity.Info.Turn = turn; - SpellsPlayedCount++; + if(entity.CardId != null) + { + SpellsPlayedCardIds.Add(entity.CardId); + } //Log(entity); } public void SigilPlayedFromHand(Entity entity, int turn) { entity.Info.Turn = turn; - SpellsPlayedCount++; + if(entity.CardId != null) + { + SpellsPlayedCardIds.Add(entity.CardId); + } if(entity.Tags.TryGetValue(GameTag.SPELL_SCHOOL, out var spellSchoolTag)) PlayedSpellSchools.Add((SpellSchool)spellSchoolTag); //Log(entity); @@ -693,7 +741,10 @@ public void SigilPlayedFromHand(Entity entity, int turn) public void ObjectivePlayedFromHand(Entity entity, int turn) { entity.Info.Turn = turn; - SpellsPlayedCount++; + if(entity.CardId != null) + { + SpellsPlayedCardIds.Add(entity.CardId); + } if(entity.Tags.TryGetValue(GameTag.SPELL_SCHOOL, out var spellSchoolTag)) PlayedSpellSchools.Add((SpellSchool)spellSchoolTag); //Log(entity); @@ -703,7 +754,7 @@ public void PlayToGraveyard(Entity entity, string cardId, int turn) { entity.Info.Turn = turn; if(entity.IsMinion) - LastDiedMinionCardId = cardId; + DeadMinionsCardIds.Add(cardId); //Log(entity); } diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/DefaultCard.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/DefaultCard.cs new file mode 100644 index 000000000..028b2af94 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/DefaultCard.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards; + +public class DefaultCard: ICardWithRelatedCards +{ + public string GetCardId() => ""; + + public bool ShouldShowForOpponent(Player opponent) => false; + + public List GetRelatedCards(Player player) => new(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/DemonHunter/ReturnPolicy.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/DemonHunter/ReturnPolicy.cs new file mode 100644 index 000000000..0e5f72e69 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/DemonHunter/ReturnPolicy.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.DemonHunter; + +public class ReturnPolicy: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Demonhunter.ReturnPolicy; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 1; + } + public List GetRelatedCards(Player player) => + player.CardsPlayedThisMatch + .Distinct() + .Select(Database.GetCardFromId) + .Where(card => card is { Mechanics: not null } && card.Mechanics.Contains("Deathrattle")) + .OrderByDescending(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Druid/HydrationStation.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Druid/HydrationStation.cs new file mode 100644 index 000000000..0e3989cd2 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Druid/HydrationStation.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using Hearthstone_Deck_Tracker.Enums; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Druid; + +public class HydrationStation: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Druid.HydrationStation; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 1; + } + + public List GetRelatedCards(Player player) => + player.DeadMinionsCardIds + .Distinct() + .Select(Database.GetCardFromId) + .Where(card => card is { Mechanics: not null } && card.Mechanics.Contains("Taunt")) + .OrderByDescending(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/PetParrot.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/PetParrot.cs new file mode 100644 index 000000000..39e7279de --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/PetParrot.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Hunter; + +public class PetParrot: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Hunter.PetParrot; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 0; + } + + public List GetRelatedCards(Player player) + { + var lastCost1 = player.CardsPlayedThisMatch + .Select(Database.GetCardFromId) + .LastOrDefault(card => card is { Cost: 1 }); + return lastCost1 != null ? new List { lastCost1 } : new List(); + } +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/Product9.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/Product9.cs new file mode 100644 index 000000000..35851a557 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/Product9.cs @@ -0,0 +1,23 @@ +using System.Linq; +using System.Collections.Generic; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Hunter; + +public class Product9: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Hunter.Product9; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 0; + } + + public List GetRelatedCards(Player player) => + player.SecretsTriggeredCardIds + .Distinct() + .Select(Database.GetCardFromId) + .Where(card => card != null) + .OrderByDescending(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/Sasquawk.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/Sasquawk.cs new file mode 100644 index 000000000..0c0b1d0bd --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/Sasquawk.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Hunter; + +public class Sasquawk: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Hunter.Sasquawk; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 0; + } + + public List GetRelatedCards(Player player) => + player.CardsPlayedLastTurn + .Select(Database.GetCardFromId) + .Where(card => card != null) + .OrderByDescending(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/StranglethornHeart.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/StranglethornHeart.cs new file mode 100644 index 000000000..5a94ecfc2 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Hunter/StranglethornHeart.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Hunter; + +public class StranglethornHeart: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Hunter.StranglethornHeart; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 1; + } + + public List GetRelatedCards(Player player) => + player.DeadMinionsCardIds + .Select(Database.GetCardFromId) + .Where(card => card != null && card.IsBeast() && card.Cost > 4) + .OrderByDescending(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Mage/Rewind.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Mage/Rewind.cs new file mode 100644 index 000000000..0a8a4c703 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Mage/Rewind.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Mage; + +public class Rewind: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Mage.Rewind; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 1; + } + + public List GetRelatedCards(Player player) => + player.SpellsPlayedCardIds + .Distinct() + .Select(Database.GetCardFromId) + .Where(card => card != null && card.Id != HearthDb.CardIds.Collectible.Mage.Rewind) + .OrderByDescending(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Mage/TheGalacticProjectionOrb.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Mage/TheGalacticProjectionOrb.cs new file mode 100644 index 000000000..512cbf499 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Mage/TheGalacticProjectionOrb.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Mage; + +public class TheGalacticProjectionOrb: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Mage.TheGalacticProjectionOrb; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 1; + } + + public List GetRelatedCards(Player player) => + player.SpellsPlayedCardIds + .Distinct() + .Select(Database.GetCardFromId) + .Where(card => card != null) + .OrderBy(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/LadyLiadrin.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/LadyLiadrin.cs new file mode 100644 index 000000000..3260c6817 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/LadyLiadrin.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Paladin; + +public class LadyLiadrin: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Paladin.LadyLiadrin; + + public bool ShouldShowForOpponent(Player opponent) => false; + + public List GetRelatedCards(Player player) => + player.SpellsPlayedInFriendlyCharacters + .Select(Database.GetCardFromId) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/LadyLiadrinCore.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/LadyLiadrinCore.cs new file mode 100644 index 000000000..446399bbc --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/LadyLiadrinCore.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Paladin; + +public class LadyLiadrinCore: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Paladin.LadyLiadrinCore; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 2; + } + + public List GetRelatedCards(Player player) => + player.SpellsPlayedInFriendlyCharacters + .Select(Database.GetCardFromId) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/Tyr.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/Tyr.cs new file mode 100644 index 000000000..cfb5748c0 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/Tyr.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Paladin; + +public class Tyr: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Paladin.Tyr; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 0; + } + + public List GetRelatedCards(Player player) => + player.DeadMinionsCardIds + .Distinct() + .Select(Database.GetCardFromId) + .Where(card => card != null && card.IsClass(player.Class) && card.Attack is > 1 and < 5) + .OrderBy(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/TyrsTears.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/TyrsTears.cs new file mode 100644 index 000000000..8aabff148 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/TyrsTears.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Paladin; + +public class TyrsTears: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Paladin.TyrsTears_TyrsTearsToken; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 1; + } + + public List GetRelatedCards(Player player) => + player.DeadMinionsCardIds + .Distinct() + .Select(Database.GetCardFromId) + .Where(card => card != null && card.IsClass(player.Class)) + .OrderBy(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/TyrsTearsForged.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/TyrsTearsForged.cs new file mode 100644 index 000000000..db1271db4 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Paladin/TyrsTearsForged.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Paladin; + +public class TyrsTearsForged: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.NonCollectible.Paladin.TyrsTears; + + public bool ShouldShowForOpponent(Player opponent) => false; + + public List GetRelatedCards(Player player) => + player.DeadMinionsCardIds + .Distinct() + .Select(Database.GetCardFromId) + .Where(card => card != null && card.IsClass(player.Class)) + .OrderBy(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Rogue/TessGreymane.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Rogue/TessGreymane.cs new file mode 100644 index 000000000..1678227ce --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Rogue/TessGreymane.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Rogue; + +public class TessGreymane: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Rogue.TessGreymane; + + public bool ShouldShowForOpponent(Player opponent) + { + return false; + } + + public List GetRelatedCards(Player player) => + player.CardsPlayedThisMatch + .Select(Database.GetCardFromId) + .Where(card => card != null && !card.IsClass(player.Class) && !card.IsNeutral) + .OrderBy(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Rogue/TessGreymaneCore.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Rogue/TessGreymaneCore.cs new file mode 100644 index 000000000..6d0c01f50 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Rogue/TessGreymaneCore.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Rogue; + +public class TessGreymaneCore: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Rogue.TessGreymaneCore; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 2; + } + + public List GetRelatedCards(Player player) => + player.CardsPlayedThisMatch + .Select(Database.GetCardFromId) + .Where(card => card != null && !card.IsClass(player.Class) && !card.IsNeutral) + .OrderBy(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Shaman/Shudderwock.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Shaman/Shudderwock.cs new file mode 100644 index 000000000..c42629b12 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Shaman/Shudderwock.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Shaman; + +public class Shudderwock: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Shaman.Shudderwock; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 3; + } + + public List GetRelatedCards(Player player) => + player.CardsPlayedThisMatch + .Select(Database.GetCardFromId) + .Where(card => card is { Mechanics: not null } && card.Mechanics.Contains("Battlecry")) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Warrior/InventorBoom.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Warrior/InventorBoom.cs new file mode 100644 index 000000000..4aac9ce93 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/Cards/Warrior/InventorBoom.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem.Cards.Warrior; + +public class InventorBoom: ICardWithRelatedCards +{ + public string GetCardId() => HearthDb.CardIds.Collectible.Warrior.InventorBoom; + + public bool ShouldShowForOpponent(Player opponent) + { + var card = Database.GetCardFromId(GetCardId()); + return CardUtils.MayCardBeRelevant(card, Core.Game.CurrentFormat, opponent.Class) && GetRelatedCards(opponent).Count > 0; + } + + public List GetRelatedCards(Player player) => + player.DeadMinionsCardIds + .Distinct() + .Select(Database.GetCardFromId) + .Where(card => card != null && card.IsMech() && card.Cost > 4) + .OrderBy(card => card!.Cost) + .ToList(); +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/ICardWithRelatedCards.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/ICardWithRelatedCards.cs new file mode 100644 index 000000000..9caea07a2 --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/ICardWithRelatedCards.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem; + +public interface ICardWithRelatedCards +{ + string GetCardId(); + bool ShouldShowForOpponent(Player opponent); + List GetRelatedCards(Player player); + +} diff --git a/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/RelatedCardsManager.cs b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/RelatedCardsManager.cs new file mode 100644 index 000000000..ab1082edc --- /dev/null +++ b/Hearthstone Deck Tracker/Hearthstone/RelatedCardsSystem/RelatedCardsManager.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Hearthstone_Deck_Tracker.Utility.Extensions; + +namespace Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem; + +public class RelatedCardsManager +{ + public readonly Dictionary Cards = new(); + + private void Initialize() + { + var cards = Assembly.GetAssembly(typeof(ICardWithRelatedCards)).GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && typeof(ICardWithRelatedCards).IsAssignableFrom(t)); + + foreach(var card in cards) + { + if (Activator.CreateInstance(card) is not ICardWithRelatedCards cardWithRelatedCards) continue; + Cards.Add(cardWithRelatedCards.GetCardId(), cardWithRelatedCards); + } + } + + public void Reset() + { + if (Cards.Count == 0) Initialize(); + } + + public ICardWithRelatedCards GetCardWithRelatedCards(string cardId) + { + return Cards.TryGetValue(cardId, out var card) ? card : Cards[""]; + } + + public IEnumerable GetCardsOpponentMayHave(Player opponent) + { + return Cards.Values.Where(card => card.ShouldShowForOpponent(opponent)) + .Select(card => Database.GetCardFromId(card.GetCardId())).WhereNotNull(); + } +} diff --git a/Hearthstone Deck Tracker/Hearthstone/Watchers.cs b/Hearthstone Deck Tracker/Hearthstone/Watchers.cs index 219205b63..02078b9bd 100644 --- a/Hearthstone Deck Tracker/Hearthstone/Watchers.cs +++ b/Hearthstone Deck Tracker/Hearthstone/Watchers.cs @@ -73,6 +73,8 @@ internal static void OnBigCardChange(object sender, HearthWatcher.EventArgs.BigC ; Core.Overlay.SetCardOpacityMask(state); + + Core.Overlay.HoveredCard = state; } internal static void OnDeckPickerChange(object sender, HearthWatcher.EventArgs.DeckPickerEventArgs args) diff --git a/Hearthstone Deck Tracker/IGameHandler.cs b/Hearthstone Deck Tracker/IGameHandler.cs index 45fea4484..8c53e2584 100644 --- a/Hearthstone Deck Tracker/IGameHandler.cs +++ b/Hearthstone Deck Tracker/IGameHandler.cs @@ -56,6 +56,7 @@ public interface IGameHandler void HandlePlayerCreateInSetAside(Entity entity, int getTurnNumber); void HandlePlayerDredge(); void HandlePlayerUnknownCardAddedToDeck(); + void HandlePlayerSecretTrigger(Entity entity, string? cardId, int turn, int otherId); #region SecretTriggers diff --git a/Hearthstone Deck Tracker/LogReader/Handlers/PowerHandler.cs b/Hearthstone Deck Tracker/LogReader/Handlers/PowerHandler.cs index 0d7d785eb..9bea26841 100644 --- a/Hearthstone Deck Tracker/LogReader/Handlers/PowerHandler.cs +++ b/Hearthstone Deck Tracker/LogReader/Handlers/PowerHandler.cs @@ -137,6 +137,7 @@ public void Handle(string logLine, IHsGameState gameState, IGame game) var zone = GameTagHelper.ParseEnum(match.Groups["zone"].Value); var guessedCardId = false; var guessedLocation = DeckLocation.Unknown; + string? copyOfCardId = null; if(!game.Entities.ContainsKey(id)) { if(string.IsNullOrEmpty(cardId) && zone != Zone.SETASIDE) @@ -146,13 +147,14 @@ public void Handle(string logLine, IHsGameState gameState, IGame game) { var known = gameState.KnownCardIds[blockId.Value].FirstOrDefault(); cardId = known.Item1; + copyOfCardId = known.Item3; if(!string.IsNullOrEmpty(cardId)) { guessedLocation = known.Item2; Log.Info($"Found data for entity={id}: CardId={cardId}, Location={guessedLocation}"); - gameState.KnownCardIds[blockId.Value].Remove(known); guessedCardId = true; } + gameState.KnownCardIds[blockId.Value].Remove(known); } } var entity = new Entity(id) { CardId = cardId }; @@ -164,6 +166,8 @@ public void Handle(string logLine, IHsGameState gameState, IGame game) var sign = guessedLocation == DeckLocation.Top ? 1 : -1; entity.Info.DeckIndex = sign * newIndex; } + entity.Info.CopyOfCardId = copyOfCardId; + game.Entities.Add(id, entity); if(gameState.CurrentBlock != null && zone == Zone.DECK) @@ -248,7 +252,44 @@ public void Handle(string logLine, IHsGameState gameState, IGame game) gameState.GameHandler?.HandlePlayerDredge(); } } + + if(entity.CardId == NonCollectible.Neutral.PhotographerFizzle_FizzlesSnapshotToken + && gameState.CurrentBlock is { CardId: Collectible.Neutral.PhotographerFizzle }) + { + if(entity.IsControlledBy(game.Player.Id)) + entity.Info.StoredCardIds.AddRange(game.Player.Hand.OrderBy(e => e.ZonePosition).Select(e => e.Card.Id)); + else if(entity.IsControlledBy(game.Opponent.Id)) + { + entity.Info.StoredCardIds.AddRange(game.Opponent.Hand.OrderBy(e => e.ZonePosition) + .Select(e => { + if(e.HasCardId && !e.Info.Hidden) + { + return e.Card.Id; + } + return e.Id.ToString(); + })); + } + } + + } + + var fizzleSnapshots = game.Opponent.PlayerEntities + .Where(e => e.CardId == NonCollectible.Neutral.PhotographerFizzle_FizzlesSnapshotToken); + + foreach(var fizzle in fizzleSnapshots) + { + if(fizzle.Info.StoredCardIds.Contains(entity.Id.ToString())) + { + var index = fizzle.Info.StoredCardIds.FindIndex(e => e == entity.Id.ToString()); + if(index != -1) + { + fizzle.Info.StoredCardIds[index] = entity.Card.Id; + } + } } + + HandleCopiedCard(game, entity); + if(type == "CHANGE_ENTITY") { if(!entity.Info.OriginalEntityWasCreated.HasValue) @@ -630,6 +671,19 @@ public void Handle(string logLine, IHsGameState gameState, IGame game) case Collectible.Rogue.MetalDetector: AddKnownCardId(gameState, NonCollectible.Neutral.TheCoinCore); break; + case Collectible.Mage.CommanderSivara: + case Collectible.Neutral.TidepoolPupil: + if( + gameState.CurrentBlock?.Parent?.CardId != null + && Database.GetCardFromId(gameState.CurrentBlock.Parent.CardId)?.Type == "Spell" + && actionStartingEntity != null + ) + { + var maxCards = 3; + if(actionStartingEntity.Info.StoredCardIds.Count() < maxCards) + actionStartingEntity.Info.StoredCardIds.Add(gameState.CurrentBlock.Parent.CardId); + } + break; case Collectible.Neutral.AugmentedElekk: if (gameState.CurrentBlock?.Parent != null) { @@ -1031,6 +1085,28 @@ public void Handle(string logLine, IHsGameState gameState, IGame game) case Collectible.Warrior.TheRyecleaver: AddKnownCardId(gameState, NonCollectible.Warrior.TheRyecleaver_SliceOfBreadToken); break; + case NonCollectible.Neutral.PhotographerFizzle_FizzlesSnapshotToken: + foreach(var card in actionStartingEntity?.Info.StoredCardIds ?? new List()) + { + // When the opponent plays the "Fizzle" card, a snapshot of the game state is captured. + // Some cards are revealed, providing their exact cardId, while others we only know the entityId. + // We handle these cases differently based on the information available: + // + // 1. If the revealed identifier is a number, it represents an entityId + // In this case, we link the created card to the existing entity. + // + // 2. If the revealed identifier is not a number, it represents a cardId + // Here, we create a new card using the known cardId. + if(int.TryParse(card, out _)) + { + AddKnownCardId(gameState, "", copyOfCardId: card); + } + else + { + AddKnownCardId(gameState, card); + } + } + break; default: if(playerEntity.Value != null && playerEntity.Value.GetTag(GameTag.CURRENT_PLAYER) == 1 && !gameState.PlayerUsedHeroPower @@ -1122,6 +1198,29 @@ public void Handle(string logLine, IHsGameState gameState, IGame game) gameState.ResetCurrentEntity(); } + private void HandleCopiedCard(IGame game, Entity entity) + { + var copyOfCard = game.Opponent.PlayerEntities + .FirstOrDefault(e => e.Info.CopyOfCardId == entity.Id.ToString()); + + if(copyOfCard != null) + { + copyOfCard.CardId = entity.CardId; + } + + if(entity.Info.CopyOfCardId != null) + { + var matchingEntity = game.Opponent.PlayerEntities + .FirstOrDefault(e => e.Id.ToString() == entity.Info.CopyOfCardId); + + if(matchingEntity != null) + { + matchingEntity.CardId = entity.CardId; + matchingEntity.Info.Hidden = false; + } + } + } + private static string EnsureValidCardID(string cardId) { if(string.IsNullOrEmpty(cardId)) @@ -1142,7 +1241,7 @@ private static string EnsureValidCardID(string cardId) return !cardIdMatch.Success ? null : cardIdMatch.Groups["cardId"].Value.Trim(); } - private static void AddKnownCardId(IHsGameState gameState, string cardId, int count = 1, DeckLocation location = DeckLocation.Unknown) + private static void AddKnownCardId(IHsGameState gameState, string cardId, int count = 1, DeckLocation location = DeckLocation.Unknown, string? copyOfCardId = null) { if(gameState.CurrentBlock == null) return; @@ -1150,8 +1249,8 @@ private static void AddKnownCardId(IHsGameState gameState, string cardId, int co for(var i = 0; i < count; i++) { if(!gameState.KnownCardIds.ContainsKey(blockId)) - gameState.KnownCardIds[blockId] = new List<(string, DeckLocation)>(); - gameState.KnownCardIds[blockId].Add((cardId, location)); + gameState.KnownCardIds[blockId] = new List<(string, DeckLocation, string?)>(); + gameState.KnownCardIds[blockId].Add((cardId, location, copyOfCardId)); } } diff --git a/Hearthstone Deck Tracker/LogReader/Handlers/TagChangeActions.cs b/Hearthstone Deck Tracker/LogReader/Handlers/TagChangeActions.cs index 1b2d90424..844e19afe 100644 --- a/Hearthstone Deck Tracker/LogReader/Handlers/TagChangeActions.cs +++ b/Hearthstone Deck Tracker/LogReader/Handlers/TagChangeActions.cs @@ -750,6 +750,13 @@ private void ZoneChangeFromSecret(IHsGameState gameState, int id, IGame game, in return; gameState.GameHandler?.HandleOpponentSecretTrigger(entity, cardId, gameState.GetTurnNumber(), id); } + else + { + if(!game.Entities.TryGetValue(id, out var entity)) + return; + if (entity.CardId != null) + gameState.GameHandler?.HandlePlayerSecretTrigger(entity, cardId, gameState.GetTurnNumber(), id); + } break; case Zone.SETASIDE: if(controller == game.Opponent.Id) diff --git a/Hearthstone Deck Tracker/LogReader/HsGameState.cs b/Hearthstone Deck Tracker/LogReader/HsGameState.cs index 530b34a5f..35e0e9206 100644 --- a/Hearthstone Deck Tracker/LogReader/HsGameState.cs +++ b/Hearthstone Deck Tracker/LogReader/HsGameState.cs @@ -33,7 +33,7 @@ public HsGameState(GameV2 game) public bool PlayerUsedHeroPower { get; set; } public bool FoundSpectatorStart { get; set; } public int JoustReveals { get; set; } - public Dictionary> KnownCardIds { get; set; } = new(); + public Dictionary> KnownCardIds { get; set; } = new(); public int LastCardPlayed { get; set; } public Stack LastPlagueDrawn { get; set; } = new(); public bool WasInProgress { get; set; } diff --git a/Hearthstone Deck Tracker/LogReader/Interfaces/IHsGameState.cs b/Hearthstone Deck Tracker/LogReader/Interfaces/IHsGameState.cs index 304b3b3ff..5c65a0287 100644 --- a/Hearthstone Deck Tracker/LogReader/Interfaces/IHsGameState.cs +++ b/Hearthstone Deck Tracker/LogReader/Interfaces/IHsGameState.cs @@ -22,7 +22,7 @@ public interface IHsGameState bool PlayerUsedHeroPower { get; set; } bool FoundSpectatorStart { get; set; } int JoustReveals { get; set; } - Dictionary> KnownCardIds { get; set; } + Dictionary> KnownCardIds { get; set; } int LastCardPlayed { get; set; } Stack LastPlagueDrawn { get; set; } bool WasInProgress { get; set; } diff --git a/Hearthstone Deck Tracker/Utility/HotKeys/PredefinedHotKeyActions.cs b/Hearthstone Deck Tracker/Utility/HotKeys/PredefinedHotKeyActions.cs index 1f1874274..ec1d3c1f9 100644 --- a/Hearthstone Deck Tracker/Utility/HotKeys/PredefinedHotKeyActions.cs +++ b/Hearthstone Deck Tracker/Utility/HotKeys/PredefinedHotKeyActions.cs @@ -133,6 +133,18 @@ public static void ToggleOverlayCounters() Core.Overlay.UpdatePosition(); } + [PredefinedHotKeyAction("Toggle overlay: Related Cards", "Turns related cards tooltips on the overlay on or off (if the game is running).") + ] + public static void ToggleOverlayRelatedCards() + { + if(!Core.Game.IsRunning) + return; + Config.Instance.HidePlayerRelatedCards = !Config.Instance.HidePlayerRelatedCards; + Config.Instance.HideOpponentRelatedCards = Config.Instance.HidePlayerRelatedCards; + Config.Save(); + Core.Overlay.UpdatePosition(); + } + [PredefinedHotKeyAction("Toggle My Games panel", "Turns on or off visibility of My Games panel.")] public static void ToggleMyGamesPanel() { diff --git a/Hearthstone Deck Tracker/Utility/ValueMoments/Actions/Action/GeneralSettings.cs b/Hearthstone Deck Tracker/Utility/ValueMoments/Actions/Action/GeneralSettings.cs index 14e85e29f..1fc21917a 100644 --- a/Hearthstone Deck Tracker/Utility/ValueMoments/Actions/Action/GeneralSettings.cs +++ b/Hearthstone Deck Tracker/Utility/ValueMoments/Actions/Action/GeneralSettings.cs @@ -61,5 +61,11 @@ public class GeneralSettings [JsonProperty("opponent_counters")] public bool OpponentCounters { get => !Config.Instance.HideOpponentCounters; } + + [JsonProperty("player_related_cards")] + public bool PlayerRelatedCards { get => !Config.Instance.HidePlayerRelatedCards; } + + [JsonProperty("opponent_related_cards")] + public bool OpponentRelatedCards { get => !Config.Instance.HideOpponentRelatedCards; } } } diff --git a/Hearthstone Deck Tracker/Windows/OverlayWindow.DeckLists.cs b/Hearthstone Deck Tracker/Windows/OverlayWindow.DeckLists.cs index e5b8a5490..ba3e614da 100644 --- a/Hearthstone Deck Tracker/Windows/OverlayWindow.DeckLists.cs +++ b/Hearthstone Deck Tracker/Windows/OverlayWindow.DeckLists.cs @@ -71,6 +71,11 @@ public void UpdateOpponentLayout() break; } } + + if(!Config.Instance.HideOpponentRelatedCards) + { + StackPanelOpponent.Children.Add(OpponentRelatedCardsDeckLens); + } } public void SetWinRates() @@ -178,6 +183,10 @@ public void UpdatePlayerCards(List cards, bool reset, List top, List PlayerSideboards.Update(sideboards, reset); } - public void UpdateOpponentCards(List cards, bool reset) => ListViewOpponent.Update(cards, reset); + public void UpdateOpponentCards(List cards, List cardsWithRelatedCards, bool reset) + { + ListViewOpponent.Update(cards, reset); + OpponentRelatedCardsDeckLens.Update(cardsWithRelatedCards.Where(card => cards.All(c => c.Id != card.Id)).ToList(), reset); + } } } diff --git a/Hearthstone Deck Tracker/Windows/OverlayWindow.Tooltips.cs b/Hearthstone Deck Tracker/Windows/OverlayWindow.Tooltips.cs index afc2cf587..e1da4c515 100644 --- a/Hearthstone Deck Tracker/Windows/OverlayWindow.Tooltips.cs +++ b/Hearthstone Deck Tracker/Windows/OverlayWindow.Tooltips.cs @@ -2,6 +2,7 @@ using System; using System.Collections.ObjectModel; +using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; @@ -9,14 +10,17 @@ using BobsBuddy.Factory; using HearthDb.Enums; using HearthMirror; +using HearthMirror.Objects; using Hearthstone_Deck_Tracker.Controls; using Hearthstone_Deck_Tracker.Controls.Overlay; using Hearthstone_Deck_Tracker.Controls.Overlay.Battlegrounds.Minions; using Hearthstone_Deck_Tracker.Controls.Overlay.Constructed.ActiveEffects; using Hearthstone_Deck_Tracker.Hearthstone; using Hearthstone_Deck_Tracker.Hearthstone.Entities; +using Hearthstone_Deck_Tracker.Utility; using Hearthstone_Deck_Tracker.Utility.Assets; using Hearthstone_Deck_Tracker.Utility.Logging; +using NuGet; using static System.Windows.Visibility; #endregion @@ -32,6 +36,8 @@ public partial class OverlayWindow private DateTime? _minionBrowserHoverStart = null; private string? _minionBrowserHoverCardId = null; + public BigCardState? HoveredCard; + private void UpdateCardTooltip() { var pos = User32.GetMousePos(); @@ -44,6 +50,7 @@ private void UpdateCardTooltip() var relativePlayerBottomDeckPos = PlayerBottomDeckLens.CardList.Items.Count > 0 ? PlayerBottomDeckLens.CardList.PointFromScreen(new Point(pos.X, pos.Y)) : new Point(-1, -1); var relativePlayerSideboardsDeckPos = PlayerSideboards.CardList.Items.Count > 0 ? PlayerSideboards.CardList.PointFromScreen(new Point(pos.X, pos.Y)) : new Point(-1, -1); var relativeOpponentDeckPos = ViewBoxOpponent.PointFromScreen(new Point(pos.X, pos.Y)); + var relativeOpponentRelatedCardsPos = OpponentRelatedCardsDeckLens.CardList.Items.Count > 0 ? OpponentRelatedCardsDeckLens.CardList.PointFromScreen(new Point(pos.X, pos.Y)) : new Point(-1, -1); var relativeSecretsPos = StackPanelSecrets.PointFromScreen(new Point(pos.X, pos.Y)); var relativeCardMark = _cardMarks.Select(x => new { Label = x, Pos = x.PointFromScreen(new Point(pos.X, pos.Y)) }); var visibility = (Config.Instance.OverlayCardToolTips && !Config.Instance.OverlaySecretToolTipsOnly) @@ -260,7 +267,7 @@ private void UpdateCardTooltip() } //player card tooltips else if(ListViewPlayer.Visibility == Visible && StackPanelPlayer.Visibility == Visible - && PointInsideControl(relativePlayerDeckPos, ListViewPlayer.ActualWidth, ListViewPlayer.ActualHeight)) + && PointInsideControl(relativePlayerDeckPos, ViewBoxPlayer.ActualWidth, ViewBoxPlayer.ActualHeight)) { //card size = card list height / amount of cards var cardSize = ViewBoxPlayer.ActualHeight / ListViewPlayer.Items.Count; @@ -283,6 +290,7 @@ private void UpdateCardTooltip() SetTooltipPosition(topOffset, BorderStackPanelPlayer); ToolTipCardBlock.Visibility = visibility; + SetRelatedCardsTooltip(Core.Game.Player, card.Id); } //player top card tooltips else if(PlayerTopDeckLens.Visibility == Visible && StackPanelPlayer.Visibility == Visible @@ -310,6 +318,7 @@ private void UpdateCardTooltip() SetTooltipPosition(topOffset, BorderStackPanelPlayer); ToolTipCardBlock.Visibility = visibility; + SetRelatedCardsTooltip(Core.Game.Player, card.Id); } //player bottom card tooltips else if(PlayerBottomDeckLens.Visibility == Visible && StackPanelPlayer.Visibility == Visible @@ -337,6 +346,7 @@ private void UpdateCardTooltip() SetTooltipPosition(topOffset, BorderStackPanelPlayer); ToolTipCardBlock.Visibility = visibility; + SetRelatedCardsTooltip(Core.Game.Player, card.Id); } //player sideboard card tooltips else if(PlayerSideboards.Visibility == Visible && StackPanelPlayer.Visibility == Visible @@ -366,10 +376,11 @@ private void UpdateCardTooltip() SetTooltipPosition(topOffset, BorderStackPanelPlayer); ToolTipCardBlock.Visibility = visibility; + SetRelatedCardsTooltip(Core.Game.Player, card.Id); } //opponent card tooltips else if(ListViewOpponent.Visibility == Visible && StackPanelOpponent.Visibility == Visible - && PointInsideControl(relativeOpponentDeckPos, ListViewOpponent.ActualWidth, ListViewOpponent.ActualHeight)) + && PointInsideControl(relativeOpponentDeckPos, ViewBoxOpponent.ActualWidth, ViewBoxOpponent.ActualHeight)) { //card size = card list height / amount of cards var cardSize = ViewBoxOpponent.ActualHeight / ListViewOpponent.Items.Count; @@ -390,6 +401,37 @@ private void UpdateCardTooltip() SetTooltipPosition(topOffset, BorderStackPanelOpponent); ToolTipCardBlock.Visibility = visibility; + SetRelatedCardsTooltip(Core.Game.Opponent, card.Id); + } + // opponent related cards tooltip + else if(OpponentRelatedCardsDeckLens.Visibility == Visible && StackPanelOpponent.Visibility == Visible + && PointInsideControl(relativeOpponentRelatedCardsPos, OpponentRelatedCardsDeckLens.ActualWidth, OpponentRelatedCardsDeckLens.ActualHeight)) + + { + //card size = card list height / amount of cards + var cardSize = OpponentRelatedCardsDeckLens.CardList.ActualHeight / OpponentRelatedCardsDeckLens.CardList.Items.Count; + var cardIndex = (int)(relativeOpponentRelatedCardsPos.Y / cardSize); + + if(cardIndex < 0 || cardIndex >= OpponentRelatedCardsDeckLens.CardList.Items.Count) + return; + + var card = OpponentRelatedCardsDeckLens.CardList.Items.Cast().ElementAt(cardIndex).Card; + ToolTipCardBlock.SetCardIdFromCard(card); + //offset is affected by scaling + var topOffset = Canvas.GetTop(BorderStackPanelOpponent) + + GetListViewOffset(StackPanelOpponent, OpponentRelatedCardsDeckLens) + + OpponentRelatedCardsDeckLens.Container.ActualHeight + + cardIndex * cardSize * Config.Instance.OverlayPlayerScaling / 100 - ToolTipCardBlock.ActualHeight/2; + + //prevent tooltip from going outside of the overlay + if(topOffset + ToolTipCardBlock.ActualHeight > Height) + topOffset = Height - ToolTipCardBlock.ActualHeight; + topOffset = Math.Max(0, topOffset); + + SetTooltipPosition(topOffset, BorderStackPanelOpponent); + + ToolTipCardBlock.Visibility = visibility; + SetRelatedCardsTooltip(Core.Game.Opponent, card.Id); } else if(StackPanelSecrets.Visibility == Visible && PointInsideControl(relativeSecretsPos, StackPanelSecrets.ActualWidth, StackPanelSecrets.ActualHeight)) @@ -510,6 +552,59 @@ private void UpdateCardTooltip() HideAdditionalToolTips(); } } + else if(HoveredCard is { IsHand: true }) + { + // Get related cards from cardId + var relatedCards = GetRelatedCards(Core.Game.Player, HoveredCard.Value.CardId, inHand: true, handPosition: HoveredCard.Value.ZonePosition); + + if (_tooltipHoverStart == null) + { + _tooltipHoverStart = DateTime.Now; + } + + var elapsed = DateTime.Now - _tooltipHoverStart.Value; + if (relatedCards.Count > 0) + { + var nonNullableRelatedCards = relatedCards.Where(c => c != null).Cast(); + + ToolTipGridCards.SetTitle(LocUtil.Get("Related_Cards", useCardLanguage: true)); + ToolTipGridCards.SetCardIdsFromCards(nonNullableRelatedCards, 470); + Canvas.SetTop(ToolTipGridCards, (480 - ToolTipGridCards.ActualHeight) * _activeEffectsScale); + + // find the left of the card + double cardTotal = HoveredCard.Value.ZoneSize > 10 ? HoveredCard.Value.ZoneSize : 10; + var baseOffsetX = 0.34; + var centerPosition = (HoveredCard.Value.ZoneSize + 1) / 2.0; + var relativePosition = HoveredCard.Value.ZonePosition - centerPosition; + var offsetXScale = HoveredCard.Value.ZoneSize > 3 ? cardTotal / HoveredCard.Value.ZoneSize * 0.037 : 0.098; + var offsetX = baseOffsetX + relativePosition * offsetXScale; + var correctedOffsetX = Helper.GetScaledXPos(offsetX, (int)Width, ScreenRatio); + + // find the center of the card + var cardHeight = 0.5; + var cardHeightInPixels = cardHeight * Height; + var cardWidth = cardHeightInPixels * 34 / (cardHeight * 100); + + Canvas.SetLeft(ToolTipGridCards, + correctedOffsetX + cardWidth / 2 - ToolTipGridCards.ActualWidth / 2 * _activeEffectsScale); + + if(elapsed.TotalMilliseconds >= TooltipDelayMilliseconds) + { + ToolTipGridCards.Visibility = Config.Instance.HidePlayerRelatedCards ? Collapsed : Visible; + } + else + { + ToolTipGridCards.Visibility = Hidden; + } + + } + else + { + ToolTipGridCards.Visibility = Hidden; + _tooltipHoverStart = null; + } + + } else { ToolTipCardBlock.SetCardIdFromCard(null); @@ -568,10 +663,72 @@ private void SetTooltipPosition(double yOffset, FrameworkElement stackpanel) { Canvas.SetTop(ToolTipCardBlock, yOffset); + if(yOffset + ToolTipGridCards.ActualHeight > Height) + { + Canvas.SetTop(ToolTipGridCards, Height - ToolTipGridCards.ActualHeight); + } + else + { + Canvas.SetTop(ToolTipGridCards, yOffset); + } + if(Canvas.GetLeft(stackpanel) < Width / 2) + { Canvas.SetLeft(ToolTipCardBlock, Canvas.GetLeft(stackpanel) + stackpanel.ActualWidth * Config.Instance.OverlayOpponentScaling / 100); + Canvas.SetLeft(ToolTipGridCards, + (Canvas.GetLeft(stackpanel) + stackpanel.ActualWidth * Config.Instance.OverlayOpponentScaling / 100) + ToolTipCardBlock.ActualWidth); + } else + { Canvas.SetLeft(ToolTipCardBlock, Canvas.GetLeft(stackpanel) - ToolTipCardBlock.ActualWidth); + Canvas.SetLeft(ToolTipGridCards, Canvas.GetLeft(stackpanel) - ToolTipCardBlock.ActualWidth - ToolTipGridCards.ActualWidth * _activeEffectsScale); + } + } + + private void SetRelatedCardsTooltip(Player player, string cardId) + { + var relatedCards = GetRelatedCards(player, cardId); + if (_tooltipHoverStart == null) + { + _tooltipHoverStart = DateTime.Now; + } + + var elapsed = DateTime.Now - _tooltipHoverStart.Value; + if (relatedCards.Count > 0 && elapsed.TotalMilliseconds >= TooltipDelayMilliseconds) + { + var nonNullableRelatedCards = relatedCards.Where(c => c != null).Cast(); + ToolTipGridCards.SetCardIdsFromCards(nonNullableRelatedCards); + ToolTipGridCards.SetTitle(LocUtil.Get("Related_Cards", useCardLanguage: true)); + ToolTipGridCards.Visibility = Visible; + } + else + { + ToolTipGridCards.Visibility = Hidden; + } + } + + private List GetRelatedCards(Player player, string cardId, bool inHand = false, int? handPosition = null) + { + var relatedCards = Core.Game.RelatedCardsManager.GetCardWithRelatedCards(cardId).GetRelatedCards(player); + // Get related cards from Entity + if (relatedCards.IsEmpty()) + { + IEnumerable entities; + if(inHand) + { + entities = handPosition != null ? player.Hand.Where(e => e.ZonePosition == handPosition) : player.Hand.Where(e => e.CardId == cardId); + } + else + { + entities = player.Deck.Where(e => e.CardId == cardId); + } + foreach(var entity in entities) + { + relatedCards.AddRange(entity.Info.StoredCardIds.Select(Database.GetCardFromId)); + } + } + + return relatedCards; } public bool PointInsideControl(Point pos, double actualWidth, double actualHeight) diff --git a/Hearthstone Deck Tracker/Windows/OverlayWindow.Update.cs b/Hearthstone Deck Tracker/Windows/OverlayWindow.Update.cs index e17ccdbf4..7318dff42 100644 --- a/Hearthstone Deck Tracker/Windows/OverlayWindow.Update.cs +++ b/Hearthstone Deck Tracker/Windows/OverlayWindow.Update.cs @@ -523,7 +523,7 @@ private void UpdateElementPositions() Canvas.SetLeft(LinkOpponentDeckDisplay, Width * Config.Instance.OpponentDeckLeft / 100); - var OpponentStackVisibleHeight = (CanvasOpponentCount.ActualHeight + CanvasOpponentChance.ActualHeight + ViewBoxOpponent.ActualHeight)* Config.Instance.OverlayOpponentScaling / 100; + var OpponentStackVisibleHeight = (CanvasOpponentCount.ActualHeight + CanvasOpponentChance.ActualHeight + ViewBoxOpponent.ActualHeight + OpponentRelatedCardsDeckLens.ActualHeight)* Config.Instance.OverlayOpponentScaling / 100; if(BorderStackPanelOpponentTop + OpponentStackVisibleHeight + 10 + LinkOpponentDeckDisplay.ActualHeight < Height) { diff --git a/Hearthstone Deck Tracker/Windows/OverlayWindow.xaml b/Hearthstone Deck Tracker/Windows/OverlayWindow.xaml index 15ee76bee..6b3cfe63a 100644 --- a/Hearthstone Deck Tracker/Windows/OverlayWindow.xaml +++ b/Hearthstone Deck Tracker/Windows/OverlayWindow.xaml @@ -87,6 +87,7 @@ ScrollViewer.CanContentScroll="False"> + @@ -262,7 +263,7 @@ DataContext="{Binding Tier7PreLobbyViewModel, RelativeSource={RelativeSource AncestorType=windows:OverlayWindow}}"/> - + diff --git a/Hearthstone Deck Tracker/Windows/OverlayWindow.xaml.cs b/Hearthstone Deck Tracker/Windows/OverlayWindow.xaml.cs index b42e3bf97..71d575b0c 100644 --- a/Hearthstone Deck Tracker/Windows/OverlayWindow.xaml.cs +++ b/Hearthstone Deck Tracker/Windows/OverlayWindow.xaml.cs @@ -307,7 +307,7 @@ public VerticalAlignment PlayerStackPanelAlignment => Config.Instance.OverlayCenterPlayerStackPanel ? VerticalAlignment.Center : VerticalAlignment.Top; public double OpponentStackHeight => (Config.Instance.OpponentDeckHeight / 100 * Height) / (Config.Instance.OverlayOpponentScaling / 100); - public double OpponentListHeight => OpponentStackHeight - OpponentLabelsHeight; + public double OpponentListHeight => OpponentStackHeight - OpponentLabelsHeight - OpponentRelatedCardsDeckLens.ActualHeight; public double OpponentLabelsHeight => CanvasOpponentChance.ActualHeight + CanvasOpponentCount.ActualHeight + LblOpponentFatigue.ActualHeight + LblWinRateAgainst.ActualHeight + ChancePanelsMargins;