Skip to content

Commit fa03d4f

Browse files
authored
Merge pull request ewrogers#30 from ewrogers/ability-conditions
Ability conditions
2 parents cdb1ea3 + 9636067 commit fa03d4f

29 files changed

+813
-161
lines changed

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,34 @@ All notable changes to this library will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [4.10.1] - 2023-06-27
8+
9+
### Added
10+
11+
- `MinHealthPercent` to ability metadata
12+
- `MaxHealthPercent` to ability metadata (Crasher/Animal Feast/Execute)
13+
- Visual indicator on spell queue when waiting on HP thresholds
14+
- `OpensDialog` to spell metadata (was only skills before)
15+
- HP threshold in ability tooltips
16+
- More tooltips in metadata editors
17+
18+
### Changed
19+
20+
- Use new metadata properties for HP based conditions on abilities
21+
- Redesign some metadata editors for skill/spells
22+
- Increase dialog size for skill/spell metadata editors
23+
- Tooltip design
24+
25+
### Removed
26+
27+
- Removed hard-coded names for HP-based skills (now is customizable)
28+
29+
### Fixed
30+
31+
- Improved dialog closing (deferred dispatcher)
32+
- Autosave load error popup & info
33+
- Thousands formatting for HP/MP
34+
735
## [4.10.0] - 2023-06-27
836

937
### Added

SleepHunter/App.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<converters:LessThanConverter x:Key="LessThanConverter"/>
1818
<converters:LessThanOrEqualConverter x:Key="LessThanOrEqualConverter"/>
1919
<converters:NumericConverter x:Key="NumericConverter"/>
20+
<converters:NotNullConverter x:Key="NotNullConverter"/>
2021
<converters:EquipmentSlotConverter x:Key="EquipmentSlotConverter"/>
2122
<converters:PlayerClassConverter x:Key="PlayerClassConverter"/>
2223
<converters:TimeSpanConverter x:Key="TimeSpanConverter"/>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using System;
2+
3+
namespace SleepHunter.Common
4+
{
5+
public readonly record struct DeferredAction(Action Action, DateTime ExecutionTime) { }
6+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
5+
namespace SleepHunter.Common
6+
{
7+
public sealed class DeferredDispatcher
8+
{
9+
private readonly ReaderWriterLockSlim readerWriterLock = new();
10+
private readonly List<DeferredAction> actions = new();
11+
12+
public DeferredDispatcher() { }
13+
14+
public void DispatchAfter(Action action, TimeSpan delay) => DispatchAt(action, DateTime.Now + delay);
15+
16+
public void DispatchAt(Action action, DateTime timestamp)
17+
{
18+
if (action == null)
19+
throw new ArgumentNullException(nameof(action));
20+
21+
readerWriterLock.EnterWriteLock();
22+
23+
try
24+
{
25+
var deferred = new DeferredAction(action, timestamp);
26+
actions.Add(deferred);
27+
}
28+
finally
29+
{
30+
readerWriterLock.ExitWriteLock();
31+
}
32+
}
33+
34+
public void Tick()
35+
{
36+
readerWriterLock.EnterUpgradeableReadLock();
37+
38+
try
39+
{
40+
for (var i = actions.Count - 1; i >= 0; i--)
41+
{
42+
var action = actions[i];
43+
44+
// If execution time is in the future, wait
45+
if (action.ExecutionTime > DateTime.Now)
46+
continue;
47+
48+
// Remove the entry from the deferred action queue
49+
readerWriterLock.EnterWriteLock();
50+
try
51+
{
52+
actions.RemoveAt(i);
53+
}
54+
finally
55+
{
56+
readerWriterLock.ExitWriteLock();
57+
}
58+
59+
// Perform the action (outside of the write lock)
60+
action.Action();
61+
}
62+
}
63+
finally
64+
{
65+
readerWriterLock.ExitUpgradeableReadLock();
66+
}
67+
}
68+
}
69+
}

SleepHunter/Converters/ComparisonConverters.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
1818

1919
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
2020
{
21-
throw new NotImplementedException();
21+
throw new NotSupportedException();
2222
}
2323
}
2424

@@ -36,7 +36,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
3636

3737
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
3838
{
39-
throw new NotImplementedException();
39+
throw new NotSupportedException();
4040
}
4141
}
4242

@@ -54,7 +54,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
5454

5555
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
5656
{
57-
throw new NotImplementedException();
57+
throw new NotSupportedException();
5858
}
5959
}
6060

@@ -72,7 +72,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
7272

7373
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
7474
{
75-
throw new NotImplementedException();
75+
throw new NotSupportedException();
7676
}
7777
}
7878

@@ -87,7 +87,20 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
8787

8888
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
8989
{
90-
throw new NotImplementedException();
90+
throw new NotSupportedException();
91+
}
92+
}
93+
94+
public sealed class NotNullConverter : IValueConverter
95+
{
96+
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
97+
{
98+
return value is not null;
99+
}
100+
101+
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
102+
{
103+
throw new NotSupportedException();
91104
}
92105
}
93106
}

SleepHunter/Converters/NumericConverter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
2525
{
2626
if (integerValue >= MillionsThreshold)
2727
{
28-
var fractionalMillions = integerValue / (double)MillionsThreshold;
28+
var fractionalMillions = integerValue / 1_000_000.0;
2929
return $"{fractionalMillions:0.0}m";
3030
}
3131
else if (integerValue >= ThousandsThreshold)
3232
{
33-
var fractionalThousands = integerValue / (double)ThousandsThreshold;
33+
var fractionalThousands = integerValue / 1_000.0;
3434
return $"{fractionalThousands:0}k";
3535
}
3636
}

SleepHunter/Macro/PlayerMacroState.cs

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using System.Linq;
44
using System.Threading;
55
using System.Windows;
6-
6+
using SleepHunter.Common;
77
using SleepHunter.Metadata;
88
using SleepHunter.Models;
99
using SleepHunter.Settings;
@@ -14,8 +14,9 @@ public sealed class PlayerMacroState : MacroState
1414
{
1515
private static readonly TimeSpan PanelTimeout = TimeSpan.FromSeconds(1);
1616
private static readonly TimeSpan SwitchDelay = TimeSpan.FromMilliseconds(100);
17-
private static readonly string[] CrasherSkillNames = new[] { "Crasher", "Animal Feast", "Execute" };
17+
private static readonly TimeSpan DialogDelay = TimeSpan.FromSeconds(2);
1818

19+
private readonly DeferredDispatcher deferredDispatcher = new();
1920
private readonly ReaderWriterLockSlim spellQueueLock = new();
2021
private readonly ReaderWriterLockSlim flowerQueueLock = new();
2122

@@ -392,6 +393,9 @@ protected override void MacroLoop(object argument)
392393
{
393394
client.Update(PlayerFieldFlags.GameClient);
394395

396+
// Tick the dispatcher so any scheduled events go off
397+
deferredDispatcher.Tick();
398+
395399
if (client.GameClient.IsUserChatting)
396400
{
397401
SetPlayerStatus(PlayerMacroStatus.ChatIsUp);
@@ -527,6 +531,10 @@ private bool DoSkillMacro(out bool didAssail)
527531
skillList.Add(skill);
528532
}
529533

534+
// Update stats for current HP if any skill might need it for evaluating min/max thresholds
535+
if (skillList.Any(skill => skill.MinHealthPercent.HasValue || skill.MaxHealthPercent.HasValue))
536+
client.Update(PlayerFieldFlags.Stats);
537+
530538
foreach (var skill in skillList.OrderBy((s) => { return s.OpensDialog; }))
531539
{
532540
client.Update(PlayerFieldFlags.GameClient);
@@ -544,17 +552,13 @@ private bool DoSkillMacro(out bool didAssail)
544552
}
545553
}
546554

547-
// Crasher skill (requires < 2% HP)
548-
if (CrasherSkillNames.Contains(skill.Name, StringComparer.OrdinalIgnoreCase))
549-
{
550-
client.Update(PlayerFieldFlags.Stats);
551-
if (client.Stats.HealthPercent >= 2)
552-
continue;
553-
554-
// TODO: Add Mad Soul + Sacrifice support!
555+
// Min health percentage (ex: > 90%), skip if cannot use YET
556+
if (skill.MinHealthPercent.HasValue && skill.MinHealthPercent.Value >= client.Stats.HealthPercent)
557+
continue;
555558

556-
// TODO: Add auto-hemloch + hemloch deum support!
557-
}
559+
// Max health percentage (ex < 2%), skip if cannot use YET
560+
if (skill.MaxHealthPercent.HasValue && client.Stats.HealthPercent > skill.MaxHealthPercent.Value)
561+
continue;
558562

559563
if (client.SwitchToPanelAndWait(skill.Panel, TimeSpan.FromSeconds(1), out var didRequireSwitch, useShiftKey))
560564
{
@@ -566,8 +570,9 @@ private bool DoSkillMacro(out bool didAssail)
566570
}
567571
}
568572

573+
// Close the dialog after a few seconds
569574
if (expectDialog)
570-
client.CancelDialog();
575+
deferredDispatcher.DispatchAfter(client.CancelDialog, DialogDelay);
571576

572577
if (useSpaceForAssail && isAssailQueued)
573578
{
@@ -604,9 +609,15 @@ private bool DoSpellMacro()
604609
return false;
605610
}
606611

607-
nextSpell.IsUndefined = !SpellMetadataManager.Instance.ContainsSpell(nextSpell.Name);
612+
var spellMetadata = SpellMetadataManager.Instance.GetSpell(nextSpell.Name);
613+
nextSpell.IsUndefined = spellMetadata == null;
614+
608615
CastSpell(nextSpell);
609616

617+
// Close the dialog after a few seconds
618+
if (spellMetadata?.OpensDialog ?? false)
619+
deferredDispatcher.DispatchAfter(client.CancelDialog, DialogDelay);
620+
610621
lastUsedSpellItem = nextSpell;
611622
lastUsedSpellItem.IsActive = true;
612623
return true;
@@ -913,32 +924,28 @@ private SpellQueueItem GetNextSpell()
913924
private SpellQueueItem GetNextSpell_NoRotation(bool skipOnCooldown = true)
914925
{
915926
if (skipOnCooldown)
916-
return spellQueue.FirstOrDefault(spell => !spell.IsOnCooldown);
927+
return spellQueue.FirstOrDefault(spell => !spell.IsWaitingOnHealth && !spell.IsOnCooldown);
917928
else
918-
return spellQueue.FirstOrDefault();
929+
return spellQueue.FirstOrDefault(spell => !spell.IsWaitingOnHealth);
919930
}
920931

921932
private SpellQueueItem GetNextSpell_SingularOrder(bool skipOnCooldown = true)
922933
{
923934
if (skipOnCooldown)
924-
return spellQueue.FirstOrDefault(spell => !spell.IsOnCooldown && !spell.IsDone);
935+
return spellQueue.FirstOrDefault(spell => !spell.IsWaitingOnHealth && !spell.IsOnCooldown && !spell.IsDone);
925936
else
926-
return spellQueue.FirstOrDefault(spell => !spell.IsDone);
937+
return spellQueue.FirstOrDefault(spell => !spell.IsWaitingOnHealth && !spell.IsDone);
927938
}
928939

929940
private SpellQueueItem GetNextSpell_RoundRobin(bool skipOnCooldown = true)
930941
{
931-
// All spells are done, nothing to cast
932-
if (spellQueue.All(spell => spell.IsDone))
933-
return null;
934-
935-
// All spells are on cooldown, and skipping so nothing to do
936-
if (spellQueue.All(spell => spell.IsOnCooldown) && skipOnCooldown)
942+
// All spells are unavailable, nothing to do
943+
if (spellQueue.All(spell => spell.IsWaitingOnHealth || spell.IsOnCooldown || spell.IsDone))
937944
return null;
938945

939946
var currentSpell = spellQueue.ElementAt(spellQueueIndex);
940947

941-
while (currentSpell.IsDone || (skipOnCooldown && currentSpell.IsOnCooldown))
948+
while (currentSpell.IsWaitingOnHealth || currentSpell.IsDone || (skipOnCooldown && currentSpell.IsOnCooldown))
942949
currentSpell = AdvanceToNextSpell();
943950

944951
// Round robin rotation for next time

SleepHunter/Metadata/SkillMetadata.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public sealed class SkillMetadata : ObservableObject
1919
private bool canImprove = true;
2020
private TimeSpan cooldown;
2121
private bool requiresDisarm;
22+
private double minHealthPercent;
23+
private double maxHealthPercent;
2224

2325
[XmlAttribute("Name")]
2426
public string Name
@@ -91,13 +93,29 @@ public double CooldownSeconds
9193
}
9294

9395
[XmlAttribute("RequiresDisarm")]
94-
[DefaultValueAttribute(false)]
96+
[DefaultValue(false)]
9597
public bool RequiresDisarm
9698
{
9799
get => requiresDisarm;
98100
set => SetProperty(ref requiresDisarm, value);
99101
}
100102

103+
[XmlAttribute("MinHealthPercent")]
104+
[DefaultValue(0)]
105+
public double MinHealthPercent
106+
{
107+
get => minHealthPercent;
108+
set => SetProperty(ref minHealthPercent, value);
109+
}
110+
111+
[XmlAttribute("MaxHealthPercent")]
112+
[DefaultValue(0)]
113+
public double MaxHealthPercent
114+
{
115+
get => maxHealthPercent;
116+
set => SetProperty(ref maxHealthPercent, value);
117+
}
118+
101119
public SkillMetadata() { }
102120

103121
public override string ToString() => Name ?? "Unknown Skill";
@@ -113,6 +131,8 @@ public void CopyTo(SkillMetadata other)
113131
other.OpensDialog = OpensDialog;
114132
other.CanImprove = CanImprove;
115133
other.RequiresDisarm = RequiresDisarm;
134+
other.MinHealthPercent = MinHealthPercent;
135+
other.MaxHealthPercent = MaxHealthPercent;
116136
}
117137
}
118138
}

0 commit comments

Comments
 (0)