Skip to content

Commit 39699fa

Browse files
authored
Merge pull request #961 from TheDeadCode/add-new-healing-calculation
Changes to healing thresholds & GCD spell interruption
2 parents df44e16 + 04cddff commit 39699fa

File tree

7 files changed

+118
-30
lines changed

7 files changed

+118
-30
lines changed

RotationSolver.Basic/Configuration/Configs.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,12 @@ public const string
320320
[ConditionBool, UI("Automatic Healing Thresholds", Filter = HealingActionCondition, Section = 1, Order = 1)]
321321
private static readonly bool _autoHeal = true;
322322

323+
/// <markdown file="Auto" name="Stop Healing Cast After Reaching Threshold" section="Healing Usage and Control" isSubsection="1">
324+
/// When enabled, you can customize the healing thresholds for when healing will be cast occur on target(s).
325+
/// </markdown>
326+
[ConditionBool, UI("Stop healing after reaching threshold. (Experimental)", Filter = HealingActionCondition, Section = 1, Order = 2, Description = "If you have another healer on the team, their healing might put the target player(s) above the healing threshold and you'll waste MP. This interrupts the cast if it happens.")]
327+
private static readonly bool _stopHealingAfterThresholdExperimental = false;
328+
323329
/// <markdown file="Auto" name="Auto-use oGCD abilities" section="Action Usage and Control" isSubsection="1">
324330
/// Whether to use oGCD abilities or not at all.
325331
/// </markdown>

RotationSolver.Basic/DataCenter.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,8 @@ private static float GetPartyMemberHPRatio(IBattleChara member)
727727
private static float _partyAvgHp = 0;
728728
private static float _partyStdDevHp = 0;
729729
private static int _partyHpCount = 0;
730+
private static float _lowestPartyAvgHp = 0;
731+
private static float _lowestPartyStdDevHp = 0;
730732

731733
private static readonly float[] _hpBuffer = new float[8];
732734
private static void UpdatePartyHpCache()
@@ -758,28 +760,47 @@ private static void UpdatePartyHpCache()
758760
_partyMinHp = 0;
759761
_partyAvgHp = 0;
760762
_partyStdDevHp = 0;
763+
_lowestPartyAvgHp = 0;
764+
_lowestPartyStdDevHp = 0;
761765
return;
762766
}
763767

768+
// If there are more than 4 players, we order the array
769+
if (hpCount > 4)
770+
{
771+
Array.Sort(_hpBuffer);
772+
}
773+
764774
float sum = 0;
775+
float lowestHpMembersSum = 0;
765776
float min = float.MaxValue;
766777
for (int i = 0; i < hpCount; i++)
767778
{
768779
sum += _hpBuffer[i];
780+
if (i < 4) lowestHpMembersSum += _hpBuffer[i];
769781
if (_hpBuffer[i] < min) min = _hpBuffer[i];
770782
}
771783

772784
float avg = sum / hpCount;
785+
float lowestHpMembersAvg = lowestHpMembersSum / (hpCount > 4 ? 4 : hpCount);
773786
float variance = 0;
787+
float lowestHpMembersVariance = 0;
774788
for (int i = 0; i < hpCount; i++)
775789
{
776790
float diff = _hpBuffer[i] - avg;
777791
variance += diff * diff;
792+
if (i < 4)
793+
{
794+
float lowestHpMembersDiff = _hpBuffer[i] - lowestHpMembersAvg;
795+
lowestHpMembersVariance += lowestHpMembersDiff * lowestHpMembersDiff;
796+
}
778797
}
779798

780799
_partyMinHp = min;
781800
_partyAvgHp = avg;
782801
_partyStdDevHp = (float)Math.Sqrt(variance / hpCount);
802+
_lowestPartyAvgHp = lowestHpMembersAvg;
803+
_lowestPartyStdDevHp = (float)Math.Sqrt(lowestHpMembersVariance / (hpCount > 4 ? 4 : hpCount));
783804
_partyHpCacheFrame = currentFrame;
784805
}
785806

@@ -798,6 +819,16 @@ public static float PartyMembersDifferHP
798819
get { UpdatePartyHpCache(); return _partyStdDevHp; }
799820
}
800821

822+
public static float LowestPartyMembersAverHP
823+
{
824+
get { UpdatePartyHpCache(); return _lowestPartyAvgHp; }
825+
}
826+
827+
public static float LowestPartyMembersDifferHP
828+
{
829+
get { UpdatePartyHpCache(); return _lowestPartyStdDevHp; }
830+
}
831+
801832
public static IEnumerable<float> PartyMembersHP
802833
{
803834
get

RotationSolver.Basic/Helpers/IActionHelper.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,43 @@ public static class IActionHelper
2121
//ActionID.SpineshatterDivePvE,
2222
ActionID.DragonfireDivePvE,
2323
};
24+
25+
internal static ActionID[] HealingActions { get; } =
26+
{
27+
// AST
28+
ActionID.BeneficIiPvE,
29+
ActionID.BeneficPvE,
30+
ActionID.BeneficPvE_21608,
31+
ActionID.HeliosConjunctionPvE,
32+
ActionID.HeliosPvE,
33+
ActionID.AspectedHeliosPvE,
34+
35+
// SGE
36+
ActionID.DiagnosisPvE,
37+
ActionID.DiagnosisPvE_26224,
38+
ActionID.PrognosisPvE,
39+
ActionID.PrognosisPvE_27043,
40+
ActionID.PneumaPvE,
41+
ActionID.PneumaPvE,
42+
43+
// WHM
44+
ActionID.CurePvE,
45+
ActionID.CureIiPvE,
46+
ActionID.CureIiPvE_21886,
47+
ActionID.MedicaPvE,
48+
ActionID.MedicaIiPvE,
49+
ActionID.MedicaIiPvE_21888,
50+
ActionID.MedicaIiiPvE,
51+
ActionID.CureIiiPvE,
52+
53+
// SCH
54+
ActionID.AdloquiumPvE,
55+
ActionID.SuccorPvE,
56+
ActionID.ConcitationPvE,
57+
ActionID.PhysickPvE,
58+
ActionID.PhysickPvE_11192,
59+
ActionID.PhysickPvE_16230
60+
};
2461

2562
/// <summary>
2663
/// Determines if the last GCD action matches any of the provided actions.
@@ -127,6 +164,27 @@ public static bool IsTheSameTo(this IAction action, bool isAdjust, params Action
127164
return action != null && actions != null && IsActionID(isAdjust ? (ActionID)action.AdjustedID : (ActionID)action.ID, actions);
128165
}
129166

167+
/// <summary>
168+
/// Searches the provided list of lists for an action ID.
169+
/// </summary>
170+
/// <param name="id">The action ID</param>
171+
/// <param name="isAdjust">Whether to use the AdjustedID parameter</param>
172+
/// <param name="lists">The list of lists of actions to search from.</param>
173+
/// <returns></returns>
174+
public static IAction? GetActionFromID(this ActionID id, bool isAdjust, params IAction[][] lists)
175+
{
176+
foreach (var list in lists)
177+
{
178+
foreach (var action in list)
179+
{
180+
if ((isAdjust && action.AdjustedID == (uint)id) ||
181+
(! isAdjust && action.ID == (uint)id)) return action;
182+
}
183+
}
184+
185+
return null;
186+
}
187+
130188
/// <summary>
131189
/// Determines if the action ID matches any of the provided action IDs.
132190
/// </summary>

RotationSolver/UI/RotationConfigWindow.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3442,6 +3442,7 @@ private static unsafe void DrawParty()
34423442
ImGui.Text($"Number of Party Members: {DataCenter.PartyMembers.Count}");
34433443
ImGui.Text($"Number of Alliance Members: {DataCenter.AllianceMembers.Count}");
34443444
ImGui.Text($"Average Party HP Percent: {DataCenter.PartyMembersAverHP * 100}");
3445+
ImGui.Text($"Average Lowest Party HP Percent: {DataCenter.LowestPartyMembersAverHP * 100}");
34453446
ImGui.Text($"Number of Party Members with Doomed To Heal status: {DataCenter.PartyMembers.Count(member => member.DoomNeedHealing())}");
34463447
foreach (Dalamud.Game.ClientState.Party.IPartyMember p in Svc.Party)
34473448
{

RotationSolver/Updaters/ActionQueueManager.cs

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -75,38 +75,16 @@ private static unsafe bool UseActionDetour(ActionManager* actionManager, uint ac
7575
var dutyActions = DataCenter.CurrentDutyRotation?.AllActions ?? [];
7676

7777
// Find matching action by ID without creating intermediate collections
78-
IAction? matchingAction = null;
7978
uint adjustedActionId = Service.GetAdjustedActionId(actionID);
8079

8180
PluginLog.Debug($"[ActionQueueManager] Detected player input: (ID: {actionID})");
8281

83-
// Search rotation actions first
84-
foreach (var action in rotationActions)
85-
{
86-
if (action.ID == adjustedActionId)
87-
{
88-
matchingAction = action;
89-
break;
90-
}
91-
}
92-
93-
// If not found, search duty actions
94-
if (matchingAction == null)
95-
{
96-
foreach (var action in dutyActions)
97-
{
98-
if (action.ID == adjustedActionId)
99-
{
100-
matchingAction = action;
101-
break;
102-
}
103-
}
104-
}
105-
106-
PluginLog.Debug($"[ActionQueueManager] Matching action decided: (ID: {matchingAction})");
82+
var matchingAction = ((ActionID)adjustedActionId).GetActionFromID(false, rotationActions, dutyActions);
10783

10884
if (matchingAction != null)
10985
{
86+
PluginLog.Debug($"[ActionQueueManager] Matching action decided: (ID: {matchingAction})");
87+
11088
if (matchingAction.IsIntercepted)
11189
{
11290
if (matchingAction.EnoughLevel && CanInterceptAction(matchingAction))

RotationSolver/Updaters/MiscUpdater.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using ECommons.DalamudServices;
55
using ECommons.ExcelServices;
66
using ECommons.GameHelpers;
7-
using FFXIVClientStructs.FFXIV.Client.Game;
87
using FFXIVClientStructs.FFXIV.Client.Game.UI;
98
using FFXIVClientStructs.FFXIV.Client.System.Framework;
109
using FFXIVClientStructs.FFXIV.Client.UI;
@@ -217,8 +216,13 @@ private static unsafe void UpdateCancelCast()
217216
}
218217

219218
bool stopDueStatus = statusTimes.Length > 0 && minStatusTime > Player.Object.TotalCastTime - Player.Object.CurrentCastTime && minStatusTime < 5;
220-
221-
if (_tarStopCastDelay.Delay(tarDead) || stopDueStatus || tarHasRaise)
219+
220+
bool shouldStopHealing = Service.Config.StopHealingAfterThresholdExperimental && DataCenter.InCombat &&
221+
DataCenter.CommandNextAction?.AdjustedID != Player.Object.CastActionId &&
222+
((ActionID)Player.Object.CastActionId).GetActionFromID(true, RotationUpdater.CurrentRotationActions) is IBaseAction {Setting.IsFriendly: true} &&
223+
(DataCenter.MergedStatus & (AutoStatus.HealAreaSpell | AutoStatus.HealSingleSpell)) == 0;
224+
225+
if (_tarStopCastDelay.Delay(tarDead) || stopDueStatus || tarHasRaise || shouldStopHealing)
222226
{
223227
UIState* uiState = UIState.Instance();
224228
if (uiState != null)

RotationSolver/Updaters/StateUpdater.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,18 @@ private static bool ShouldAddHealAreaAbility()
246246

247247
if (!canHealAreaAbility)
248248
{
249-
canHealAreaAbility = DataCenter.PartyMembersDifferHP < Service.Config.HealthDifference
250-
&& DataCenter.PartyMembersAverHP < Lerp(Service.Config.HealthAreaAbility, Service.Config.HealthAreaAbilityHot, ratio);
249+
// If party is larger than 4 people, we select the 4 lowest HP players
250+
// in the party, and then calculate the thresholds on them instead.
251+
if (DataCenter.PartyMembers.Count > 4)
252+
{
253+
canHealAreaAbility = DataCenter.LowestPartyMembersDifferHP < Service.Config.HealthDifference
254+
&& DataCenter.LowestPartyMembersAverHP < Lerp(Service.Config.HealthAreaAbility, Service.Config.HealthAreaAbilityHot, ratio);
255+
}
256+
else
257+
{
258+
canHealAreaAbility = DataCenter.PartyMembersDifferHP < Service.Config.HealthDifference
259+
&& DataCenter.PartyMembersAverHP < Lerp(Service.Config.HealthAreaAbility, Service.Config.HealthAreaAbilityHot, ratio);
260+
}
251261
}
252262
}
253263

0 commit comments

Comments
 (0)