Skip to content

Commit ebec50c

Browse files
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
1 parent c11570e commit ebec50c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+948
-67
lines changed

HDTTests/Hearthstone/Secrets/MockGame.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Hearthstone_Deck_Tracker.Hearthstone;
88
using Hearthstone_Deck_Tracker.Hearthstone.CounterSystem;
99
using Hearthstone_Deck_Tracker.Hearthstone.Entities;
10+
using Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem;
1011
using Hearthstone_Deck_Tracker.Hearthstone.Secrets;
1112
using Hearthstone_Deck_Tracker.Stats;
1213
using Card = Hearthstone_Deck_Tracker.Hearthstone.Card;
@@ -28,6 +29,7 @@ public MockGame()
2829
public Entity PlayerEntity { get; set; }
2930
public Entity OpponentEntity { get; set; }
3031
public CounterManager CounterManager { get; set; }
32+
public RelatedCardsManager RelatedCardsManager { get; set; }
3133
public bool IsMulliganDone { get; set; }
3234
public bool IsInMenu { get; set; }
3335
public bool IsUsingPremade { get; set; }

HDTTests/Utility/ValueMoments/Actions/VMActionTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ public void VMAction_MixpanelPayloadReturnsCorrect()
8383
"player_wotog_counters",
8484
"opponent_wotog_counters",
8585
"player_counters",
86-
"opponent_counters"
86+
"opponent_counters",
87+
"player_related_cards",
88+
"opponent_related_cards"
8789
}},
8890
{ "hdt_general_settings_disabled", new []{
8991
"overlay_hide_completely",

Hearthstone Deck Tracker/Config.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,9 @@ public class Config
407407
[DefaultValue(false)]
408408
public bool HideOpponentCounters = false;
409409

410+
[DefaultValue(false)]
411+
public bool HideOpponentRelatedCards = false;
412+
410413
[DefaultValue(DisplayMode.Auto)]
411414
public DisplayMode OpponentCthunCounter = DisplayMode.Auto;
412415

@@ -467,6 +470,9 @@ public class Config
467470
[DefaultValue(false)]
468471
public bool HidePlayerCounters = false;
469472

473+
[DefaultValue(false)]
474+
public bool HidePlayerRelatedCards = false;
475+
470476
[DefaultValue(true)]
471477
public bool DisablePlayerWotogs = true;
472478

Hearthstone Deck Tracker/Controls/GridCardImages.xaml.cs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,20 @@ public GridCardImages()
7777
}
7878
private IEnumerable<Hearthstone.Card>? _previousCards;
7979
Storyboard? ExpandAnimation => FindResource("AnimateGrid") as Storyboard;
80-
public async void SetCardIdsFromCards(IEnumerable<Hearthstone.Card>? cards)
80+
public async void SetCardIdsFromCards(IEnumerable<Hearthstone.Card>? cards, int? maxGridHeight = null)
8181
{
82-
if (cards == null || (_previousCards != null && _previousCards.SequenceEqual(cards)))
82+
if(cards == null || (_previousCards != null && _previousCards.SequenceEqual(cards)))
83+
{
84+
if((maxGridHeight.HasValue && maxGridHeight != _maxGridHeight)
85+
|| (!maxGridHeight.HasValue && GridHeight != _maxGridHeight))
86+
{
87+
_maxGridHeight = maxGridHeight ?? GridHeight;
88+
CardsCollectionChanged(_maxGridHeight);
89+
ExpandAnimation?.Begin();
90+
}
91+
8392
return;
93+
}
8494

8595
_previousCards = cards;
8696

@@ -122,7 +132,7 @@ public async void SetCardIdsFromCards(IEnumerable<Hearthstone.Card>? cards)
122132
Cards.Add(cardWithImage);
123133
}
124134

125-
CardsCollectionChanged();
135+
CardsCollectionChanged(maxGridHeight);
126136

127137
// Clear the loading image source after all cards are processed
128138
LoadingImageSource = null;
@@ -132,7 +142,7 @@ public async void SetCardIdsFromCards(IEnumerable<Hearthstone.Card>? cards)
132142
ExpandAnimation?.Begin();
133143
}
134144

135-
private void CardsCollectionChanged()
145+
private void CardsCollectionChanged(int? maxGridHeight = null)
136146
{
137147
var cardCount = Cards.Count;
138148
if (cardCount == 0)
@@ -150,6 +160,13 @@ private void CardsCollectionChanged()
150160
else
151161
cardHeight = (int)(cardWidth / cardRatio);
152162

163+
if (maxGridHeight.HasValue && cardHeight * rows > maxGridHeight.Value)
164+
{
165+
var scaleFactor = (double)maxGridHeight.Value / (cardHeight * rows);
166+
cardWidth = (int)(cardWidth * scaleFactor);
167+
cardHeight = (int)(cardHeight * scaleFactor);
168+
}
169+
153170
CardWidth = Math.Min(cardWidth, (int)MaxCardWidth);
154171
CardHeight = Math.Min(cardHeight, (int)MaxCardHeight);
155172
}
@@ -165,6 +182,8 @@ public void SetTitle(string title)
165182

166183
private const double MaxCardWidth = 256 * 0.75;
167184
private const double MaxCardHeight = 388 * 0.75;
185+
186+
private int _maxGridHeight = GridHeight;
168187
public Thickness CardMargin => CalculateCardMargin();
169188

170189
private Thickness CalculateCardMargin()

Hearthstone Deck Tracker/Core.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,8 @@ internal static async void UpdateOpponentCards(bool reset = false)
453453
_updateRequestsOpponent--;
454454
if(_updateRequestsOpponent > 0)
455455
return;
456-
Overlay.UpdateOpponentCards(new List<Card>(Game.Opponent.OpponentCardList), reset);
456+
var cardWithRelatedCards = Game.RelatedCardsManager.GetCardsOpponentMayHave(Game.Opponent).ToList();
457+
Overlay.UpdateOpponentCards(new List<Card>(Game.Opponent.OpponentCardList), cardWithRelatedCards, reset);
457458
if(Windows.OpponentWindow.IsVisible)
458459
Windows.OpponentWindow.UpdateOpponentCards(new List<Card>(Game.Opponent.OpponentCardList), reset);
459460
}

Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayOpponent.xaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,11 @@
197197
Margin="10,5,0,0" VerticalAlignment="Top"
198198
Checked="CheckBoxCounters_Checked"
199199
Unchecked="CheckBoxCounters_Unchecked" />
200+
<CheckBox x:Name="CheckBoxRelatedCards"
201+
Content="{lex:LocText Options_Overlay_Player_CheckBox_Related_Cards}" HorizontalAlignment="Left"
202+
Margin="10,5,0,0" VerticalAlignment="Top"
203+
Checked="CheckBoxRelatedCards_Checked"
204+
Unchecked="CheckBoxRelatedCards_Unchecked" />
200205
<CheckBox x:Name="CheckBoxAttack"
201206
Content="{lex:LocText Options_Overlay_Opponent_CheckBox_Attack}" HorizontalAlignment="Left"
202207
Margin="10,5,0,0" VerticalAlignment="Top"

Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayOpponent.xaml.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public void Load()
8282
CheckBoxAttack.IsChecked = !Config.Instance.HideOpponentAttackIcon;
8383
CheckBoxActiveEffects.IsChecked = !Config.Instance.HideOpponentActiveEffects;
8484
CheckBoxCounters.IsChecked = !Config.Instance.HideOpponentCounters;
85+
CheckBoxRelatedCards.IsChecked = !Config.Instance.HideOpponentRelatedCards;
8586
CheckboxEnableWotogs.IsChecked = !Config.Instance.DisableOpponentWotogs;
8687
ComboBoxCthun.ItemsSource = Enum.GetValues(typeof(DisplayMode)).Cast<DisplayMode>();
8788
ComboBoxCthun.SelectedItem = Config.Instance.OpponentCthunCounter;
@@ -344,6 +345,22 @@ private void CheckBoxCounters_Unchecked(object sender, RoutedEventArgs e)
344345
Config.Save();
345346
}
346347

348+
private void CheckBoxRelatedCards_Checked(object sender, RoutedEventArgs e)
349+
{
350+
if(!_initialized)
351+
return;
352+
Config.Instance.HideOpponentRelatedCards = false;
353+
Config.Save();
354+
}
355+
356+
private void CheckBoxRelatedCards_Unchecked(object sender, RoutedEventArgs e)
357+
{
358+
if(!_initialized)
359+
return;
360+
Config.Instance.HideOpponentRelatedCards = true;
361+
Config.Save();
362+
}
363+
347364
private void CheckBoxWotogs_Checked(object sender, RoutedEventArgs e)
348365
{
349366
if(!_initialized)

Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayPlayer.xaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@
161161
Margin="10,5,0,0" VerticalAlignment="Top"
162162
Checked="CheckBoxCounters_Checked"
163163
Unchecked="CheckBoxCounters_Unchecked" />
164+
<CheckBox x:Name="CheckBoxRelatedCards"
165+
Content="{lex:LocText Options_Overlay_Player_CheckBox_Related_Cards}" HorizontalAlignment="Left"
166+
Margin="10,5,0,0" VerticalAlignment="Top"
167+
Checked="CheckBoxRelatedCards_Checked"
168+
Unchecked="CheckBoxRelatedCards_Unchecked" />
164169
<CheckBox x:Name="CheckBoxAttack"
165170
Content="{lex:LocText Options_Overlay_Player_CheckBox_Attack}" HorizontalAlignment="Left"
166171
Margin="10,5,0,0" VerticalAlignment="Top"

Hearthstone Deck Tracker/FlyoutControls/Options/Overlay/OverlayPlayer.xaml.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public void Load(GameV2 game)
6666
CheckBoxCenterDeckVertically.IsChecked = Config.Instance.OverlayCenterPlayerStackPanel;
6767
CheckBoxActiveEffects.IsChecked = !Config.Instance.HidePlayerActiveEffects;
6868
CheckBoxCounters.IsChecked = !Config.Instance.HidePlayerCounters;
69+
CheckBoxRelatedCards.IsChecked = !Config.Instance.HidePlayerRelatedCards;
6970
CheckboxEnableWotogs.IsChecked = !Config.Instance.DisablePlayerWotogs;
7071
CheckBoxAttack.IsChecked = !Config.Instance.HidePlayerAttackIcon;
7172
ComboBoxCthun.ItemsSource = Enum.GetValues(typeof(DisplayMode)).Cast<DisplayMode>();
@@ -359,6 +360,22 @@ private void CheckBoxCounters_Unchecked(object sender, RoutedEventArgs e)
359360
Config.Save();
360361
}
361362

363+
private void CheckBoxRelatedCards_Checked(object sender, RoutedEventArgs e)
364+
{
365+
if(!_initialized)
366+
return;
367+
Config.Instance.HidePlayerRelatedCards = false;
368+
Config.Save();
369+
}
370+
371+
private void CheckBoxRelatedCards_Unchecked(object sender, RoutedEventArgs e)
372+
{
373+
if(!_initialized)
374+
return;
375+
Config.Instance.HidePlayerRelatedCards = true;
376+
Config.Save();
377+
}
378+
362379
private void CheckBoxWotogs_Checked(object sender, RoutedEventArgs e)
363380
{
364381
if(!_initialized)

Hearthstone Deck Tracker/GameEventHandler.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1747,6 +1747,13 @@ public void HandlePlayerSecretPlayed(Entity entity, string cardId, int turn, Zon
17471747
}
17481748
}
17491749

1750+
public void HandlePlayerSecretTrigger(Entity entity, string? cardId, int turn, int otherId)
1751+
{
1752+
if (!entity.IsSecret)
1753+
return;
1754+
_game.Player.SecretTriggered(entity, turn);
1755+
}
1756+
17501757
public void HandlePlayerHandDiscard(Entity entity, string cardId, int turn)
17511758
{
17521759
if(string.IsNullOrEmpty(cardId))
@@ -2196,7 +2203,7 @@ public void HandleOpponentSecretTrigger(Entity entity, string? cardId, int turn,
21962203
{
21972204
if (!entity.IsSecret)
21982205
return;
2199-
_game.Opponent.SecretTriggered(entity, turn);
2206+
_game.Opponent.OpponentSecretTriggered(entity, turn);
22002207
_game.SecretsManager.RemoveSecret(entity);
22012208
Core.UpdateOpponentCards();
22022209
var card = Database.GetCardFromId(cardId);
@@ -2254,6 +2261,7 @@ public void HandleQuestRewardDatabaseId(int id, int value)
22542261
void IGameHandler.HandlePlayerDraw(Entity entity, string cardId, int turn) => HandlePlayerDraw(entity, cardId, turn);
22552262
void IGameHandler.HandlePlayerMulligan(Entity entity, string cardId) => HandlePlayerMulligan(entity, cardId);
22562263
void IGameHandler.HandlePlayerSecretPlayed(Entity entity, string cardId, int turn, Zone fromZone, string parentBlockCardId) => HandlePlayerSecretPlayed(entity, cardId, turn, fromZone, parentBlockCardId);
2264+
void IGameHandler.HandlePlayerSecretTrigger(Entity entity, string? cardId, int turn, int otherId) => HandlePlayerSecretTrigger(entity, cardId, turn, otherId);
22572265
void IGameHandler.HandlePlayerHandDiscard(Entity entity, string cardId, int turn) => HandlePlayerHandDiscard(entity, cardId, turn);
22582266
void IGameHandler.HandlePlayerPlay(Entity entity, string cardId, int turn, string parentBlockCardId) => HandlePlayerPlay(entity, cardId, turn, parentBlockCardId);
22592267
void IGameHandler.HandlePlayerDeckDiscard(Entity entity, string cardId, int turn) => HandlePlayerDeckDiscard(entity, cardId, turn);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using HearthDb.Enums;
4+
using Hearthstone_Deck_Tracker.Enums;
5+
6+
namespace Hearthstone_Deck_Tracker.Hearthstone;
7+
8+
public static class CardUtils
9+
{
10+
public static IEnumerable<Card?> FilterCardsByFormat(this IEnumerable<Card?> cards, Format? format)
11+
{
12+
return cards.Where(card => IsCardFromFormat(card, format));
13+
}
14+
15+
public static bool IsCardFromFormat(Card? card, Format? format)
16+
{
17+
return format switch
18+
{
19+
Format.Classic => card != null && Helper.ClassicOnlySets.Contains(card.Set),
20+
Format.Wild => card != null && !Helper.ClassicOnlySets.Contains(card.Set),
21+
Format.Standard => card != null && !Helper.WildOnlySets.Contains(card.Set) && !Helper.ClassicOnlySets.Contains(card.Set),
22+
Format.Twist => card != null && Helper.TwistSets.Contains(card.Set),
23+
_ => true
24+
};
25+
}
26+
27+
public static IEnumerable<Card?> FilterCardsByPlayerClass(this IEnumerable<Card?> cards, string? playerClass, bool ignoreNeutral = false)
28+
{
29+
return cards.Where(card => IsCardFromPlayerClass(card, playerClass, ignoreNeutral));
30+
}
31+
32+
public static bool IsCardFromPlayerClass(Card? card, string? playerClass, bool ignoreNeutral = false)
33+
{
34+
return card != null &&
35+
(card.PlayerClass == playerClass || card.GetTouristVisitClass() == playerClass ||
36+
(!ignoreNeutral && card.CardClass == CardClass.NEUTRAL));
37+
}
38+
39+
public static bool MayCardBeRelevant(Card? card, Format? format, string? playerClass,
40+
bool ignoreNeutral = false)
41+
{
42+
return IsCardFromFormat(card, format) && IsCardFromPlayerClass(card, playerClass, ignoreNeutral);
43+
}
44+
}

Hearthstone Deck Tracker/Hearthstone/CounterSystem/BaseCounter.cs

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -71,47 +71,14 @@ protected List<string> GetCardsInDeckOrKnown(string[] cardIds)
7171

7272
protected string[] FilterCardsByClassAndFormat(string[] cardIds, string? playerClass, bool ignoreNeutral = false)
7373
{
74-
var filteredByFormat = FilterCardsByFormat(cardIds);
75-
76-
var cardsToDisplay = filteredByFormat
74+
return cardIds
7775
.Select(Database.GetCardFromId)
78-
.Where(card =>
79-
card != null &&
80-
(card.PlayerClass == playerClass || (card.GetTouristVisitClass() == playerClass) ||
81-
!ignoreNeutral && card.CardClass == CardClass.NEUTRAL))
82-
.Select(card => card!.Id).ToArray();
83-
84-
return cardsToDisplay;
76+
.FilterCardsByFormat(Game.CurrentFormat)!
77+
.FilterCardsByPlayerClass(playerClass, ignoreNeutral)
78+
.Select(card => card!.Id)
79+
.ToArray();
8580
}
8681

87-
private string[] FilterCardsByFormat(string[] cardIds)
88-
{
89-
switch(Game.CurrentFormat)
90-
{
91-
case Format.Classic:
92-
return cardIds
93-
.Select(Database.GetCardFromId)
94-
.Where(card => card != null && Helper.ClassicOnlySets.Contains(card.Set))
95-
.Select(card => card!.Id).ToArray();
96-
case Format.Wild:
97-
return cardIds
98-
.Select(Database.GetCardFromId)
99-
.Where(card => card != null && !Helper.ClassicOnlySets.Contains(card.Set))
100-
.Select(card => card!.Id).ToArray();
101-
case Format.Standard:
102-
return cardIds
103-
.Select(Database.GetCardFromId)
104-
.Where(card => card != null && !Helper.WildOnlySets.Contains(card.Set) && !Helper.ClassicOnlySets.Contains(card.Set))
105-
.Select(card => card!.Id).ToArray();
106-
case Format.Twist:
107-
return cardIds
108-
.Select(Database.GetCardFromId)
109-
.Where(card => card != null && Helper.TwistSets.Contains(card.Set))
110-
.Select(card => card!.Id).ToArray();
111-
default:
112-
return cardIds;
113-
}
114-
}
11582

11683
public event EventHandler? CounterChanged;
11784

Hearthstone Deck Tracker/Hearthstone/Entities/Entity.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,8 @@ public EntityInfo CloneWithNewEntity(Entity entity)
279279
GuessedCardState = GuessedCardState,
280280
LatestCardId = LatestCardId,
281281
StoredCardIds = StoredCardIds,
282-
DeckIndex = DeckIndex
282+
DeckIndex = DeckIndex,
283+
CopyOfCardId = CopyOfCardId
283284
};
284285
}
285286

@@ -355,6 +356,7 @@ public int GetCreatorId()
355356
public bool? OriginalEntityWasCreated { get; internal set; }
356357
public GuessedCardState GuessedCardState { get; set; } = GuessedCardState.None;
357358
public List<string> StoredCardIds { get; set; } = new List<string>();
359+
public string? CopyOfCardId { get; set; }
358360
public int DeckIndex { get; set; }
359361
public bool InGraveardAtStartOfGame { get; set; }
360362

@@ -400,6 +402,8 @@ public override string ToString()
400402
sb.Append(", deckIndex=" + DeckIndex);
401403
if(Forged)
402404
sb.Append(", forged=true");
405+
if(CopyOfCardId != null)
406+
sb.Append(", copyOf=" + CopyOfCardId);
403407
return sb.ToString();
404408
}
405409
}

Hearthstone Deck Tracker/Hearthstone/GameV2.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using Hearthstone_Deck_Tracker.Hearthstone.CounterSystem;
1919
using Hearthstone_Deck_Tracker.Hearthstone.EffectSystem;
2020
using Hearthstone_Deck_Tracker.Hearthstone.Entities;
21+
using Hearthstone_Deck_Tracker.Hearthstone.RelatedCardsSystem;
2122
using Hearthstone_Deck_Tracker.Hearthstone.Secrets;
2223
using Hearthstone_Deck_Tracker.HsReplay;
2324
using Hearthstone_Deck_Tracker.Live;
@@ -56,6 +57,7 @@ public class GameV2 : IGame
5657
public GameMetrics Metrics { get; private set; } = new();
5758
public ActiveEffects ActiveEffects { get; }
5859
public CounterManager CounterManager { get; }
60+
public RelatedCardsManager RelatedCardsManager { get; }
5961
public GameV2()
6062
{
6163
Player = new Player(this, true);
@@ -64,6 +66,7 @@ public GameV2()
6466
SecretsManager = new SecretsManager(this, new RemoteArenaSettings());
6567
ActiveEffects = new ActiveEffects();
6668
CounterManager = new CounterManager(this);
69+
RelatedCardsManager = new RelatedCardsManager();
6770
_battlegroundsBoardState = new BattlegroundsBoardState(this);
6871
_battlegroundsHeroLatestTavernUpTurn = new Dictionary<int, Dictionary<int, int>>();
6972
_battlegroundsHeroTriplesByTier = new Dictionary<int, Dictionary<int, int>>();
@@ -367,6 +370,7 @@ public void Reset(bool resetStats = true)
367370
Player.Reset();
368371
Opponent.Reset();
369372
ActiveEffects.Reset();
373+
RelatedCardsManager.Reset();
370374
if(!_matchInfoCacheInvalid && MatchInfo?.LocalPlayer != null && MatchInfo.OpposingPlayer != null)
371375
UpdatePlayers(MatchInfo);
372376
ProposedAttacker = 0;

0 commit comments

Comments
 (0)