Skip to content

Commit 04cddff

Browse files
committed
Added a tail mean calculation in full parties for healing thresholds.
Added heal GCD spell interruption if going over thresholds.
1 parent bcbefd6 commit 04cddff

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
@@ -315,6 +315,12 @@ public const string
315315
[ConditionBool, UI("Automatic Healing Thresholds", Filter = HealingActionCondition, Section = 1, Order = 1)]
316316
private static readonly bool _autoHeal = true;
317317

318+
/// <markdown file="Auto" name="Stop Healing Cast After Reaching Threshold" section="Healing Usage and Control" isSubsection="1">
319+
/// When enabled, you can customize the healing thresholds for when healing will be cast occur on target(s).
320+
/// </markdown>
321+
[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.")]
322+
private static readonly bool _stopHealingAfterThresholdExperimental = false;
323+
318324
/// <markdown file="Auto" name="Auto-use oGCD abilities" section="Action Usage and Control" isSubsection="1">
319325
/// Whether to use oGCD abilities or not at all.
320326
/// </markdown>

RotationSolver.Basic/DataCenter.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,8 @@ private static float GetPartyMemberHPRatio(IBattleChara member)
715715
private static float _partyAvgHp = 0;
716716
private static float _partyStdDevHp = 0;
717717
private static int _partyHpCount = 0;
718+
private static float _lowestPartyAvgHp = 0;
719+
private static float _lowestPartyStdDevHp = 0;
718720

719721
private static readonly float[] _hpBuffer = new float[8];
720722
private static void UpdatePartyHpCache()
@@ -746,28 +748,47 @@ private static void UpdatePartyHpCache()
746748
_partyMinHp = 0;
747749
_partyAvgHp = 0;
748750
_partyStdDevHp = 0;
751+
_lowestPartyAvgHp = 0;
752+
_lowestPartyStdDevHp = 0;
749753
return;
750754
}
751755

756+
// If there are more than 4 players, we order the array
757+
if (hpCount > 4)
758+
{
759+
Array.Sort(_hpBuffer);
760+
}
761+
752762
float sum = 0;
763+
float lowestHpMembersSum = 0;
753764
float min = float.MaxValue;
754765
for (int i = 0; i < hpCount; i++)
755766
{
756767
sum += _hpBuffer[i];
768+
if (i < 4) lowestHpMembersSum += _hpBuffer[i];
757769
if (_hpBuffer[i] < min) min = _hpBuffer[i];
758770
}
759771

760772
float avg = sum / hpCount;
773+
float lowestHpMembersAvg = lowestHpMembersSum / (hpCount > 4 ? 4 : hpCount);
761774
float variance = 0;
775+
float lowestHpMembersVariance = 0;
762776
for (int i = 0; i < hpCount; i++)
763777
{
764778
float diff = _hpBuffer[i] - avg;
765779
variance += diff * diff;
780+
if (i < 4)
781+
{
782+
float lowestHpMembersDiff = _hpBuffer[i] - lowestHpMembersAvg;
783+
lowestHpMembersVariance += lowestHpMembersDiff * lowestHpMembersDiff;
784+
}
766785
}
767786

768787
_partyMinHp = min;
769788
_partyAvgHp = avg;
770789
_partyStdDevHp = (float)Math.Sqrt(variance / hpCount);
790+
_lowestPartyAvgHp = lowestHpMembersAvg;
791+
_lowestPartyStdDevHp = (float)Math.Sqrt(lowestHpMembersVariance / (hpCount > 4 ? 4 : hpCount));
771792
_partyHpCacheFrame = currentFrame;
772793
}
773794

@@ -786,6 +807,16 @@ public static float PartyMembersDifferHP
786807
get { UpdatePartyHpCache(); return _partyStdDevHp; }
787808
}
788809

810+
public static float LowestPartyMembersAverHP
811+
{
812+
get { UpdatePartyHpCache(); return _lowestPartyAvgHp; }
813+
}
814+
815+
public static float LowestPartyMembersDifferHP
816+
{
817+
get { UpdatePartyHpCache(); return _lowestPartyStdDevHp; }
818+
}
819+
789820
public static IEnumerable<float> PartyMembersHP
790821
{
791822
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
@@ -3439,6 +3439,7 @@ private static unsafe void DrawParty()
34393439
ImGui.Text($"Number of Party Members: {DataCenter.PartyMembers.Count}");
34403440
ImGui.Text($"Number of Alliance Members: {DataCenter.AllianceMembers.Count}");
34413441
ImGui.Text($"Average Party HP Percent: {DataCenter.PartyMembersAverHP * 100}");
3442+
ImGui.Text($"Average Lowest Party HP Percent: {DataCenter.LowestPartyMembersAverHP * 100}");
34423443
ImGui.Text($"Number of Party Members with Doomed To Heal status: {DataCenter.PartyMembers.Count(member => member.DoomNeedHealing())}");
34433444
foreach (Dalamud.Game.ClientState.Party.IPartyMember p in Svc.Party)
34443445
{

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)